Skip to content

Commit 4b78ae4

Browse files
committed
PTHMINT-108: Add coverage for scoped auth changes
1 parent f922dea commit 4b78ae4

17 files changed

Lines changed: 1351 additions & 0 deletions

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ source =
44
tests
55
omit =
66
env/
7+
examples/

tests/multisafepay/unit/api/base/test_unit_decorator.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,94 @@ def test_initialization_with_empty_dependencies():
1515
"""Test the initialization of a Decorator object with empty dependencies."""
1616
decorator = Decorator()
1717
assert decorator.get_dependencies() == {}
18+
19+
20+
def test_adapt_costs_with_valid_data() -> None:
21+
"""Adapt costs from a list of cost dictionaries."""
22+
decorator = Decorator(dependencies={"key": "value"})
23+
costs = [{"amount": 100, "description": "Shipping"}]
24+
result = decorator.adapt_costs(costs)
25+
assert result is decorator
26+
assert "costs" in decorator.get_dependencies()
27+
28+
29+
def test_adapt_costs_with_none() -> None:
30+
"""Skip costs adaptation when input is None."""
31+
decorator = Decorator(dependencies={})
32+
decorator.adapt_costs(None)
33+
assert "costs" not in decorator.get_dependencies()
34+
35+
36+
def test_adapt_custom_info_with_valid_data() -> None:
37+
"""Adapt custom info from dictionary."""
38+
decorator = Decorator(dependencies={})
39+
decorator.adapt_custom_info({"custom_1": "val1"})
40+
assert "custom_info" in decorator.get_dependencies()
41+
42+
43+
def test_adapt_customer_with_valid_data() -> None:
44+
"""Adapt customer from dictionary."""
45+
decorator = Decorator(dependencies={})
46+
decorator.adapt_customer({"first_name": "John", "last_name": "Doe"})
47+
assert "customer" in decorator.get_dependencies()
48+
49+
50+
def test_adapt_payment_details_with_valid_data() -> None:
51+
"""Adapt payment details from dictionary."""
52+
decorator = Decorator(dependencies={})
53+
decorator.adapt_payment_details({"type": "VISA"})
54+
assert "payment_details" in decorator.get_dependencies()
55+
56+
57+
def test_adapt_payment_methods_with_valid_data() -> None:
58+
"""Adapt payment methods from list of dictionaries."""
59+
decorator = Decorator(dependencies={})
60+
decorator.adapt_payment_methods([{"type": "VISA"}])
61+
assert "payment_methods" in decorator.get_dependencies()
62+
63+
64+
def test_adapt_shopping_cart_with_valid_data() -> None:
65+
"""Adapt shopping cart from dictionary."""
66+
decorator = Decorator(dependencies={})
67+
decorator.adapt_shopping_cart({"items": []})
68+
assert "shopping_cart" in decorator.get_dependencies()
69+
70+
71+
def test_adapt_related_transactions_with_valid_data() -> None:
72+
"""Adapt related transactions from list of dictionaries."""
73+
decorator = Decorator(dependencies={})
74+
decorator.adapt_related_transactions(
75+
[{"transaction_id": 123, "order_id": "order-1"}],
76+
)
77+
assert "related_transactions" in decorator.get_dependencies()
78+
79+
80+
def test_adapt_checkout_options_with_valid_data() -> None:
81+
"""Adapt checkout options from dictionary."""
82+
decorator = Decorator(dependencies={})
83+
decorator.adapt_checkout_options({})
84+
assert "checkout_options" in decorator.get_dependencies()
85+
86+
87+
def test_adapt_order_adjustment_with_valid_data() -> None:
88+
"""Adapt order adjustment from dictionary."""
89+
decorator = Decorator(dependencies={})
90+
decorator.adapt_order_adjustment({"total_adjustment": 0})
91+
assert "order_adjustment" in decorator.get_dependencies()
92+
93+
94+
def test_chaining_multiple_adapters() -> None:
95+
"""Chain multiple adapt calls and get all dependencies."""
96+
deps = (
97+
Decorator(dependencies={"status": "completed"})
98+
.adapt_costs([{"amount": 10}])
99+
.adapt_custom_info({"custom_1": "a"})
100+
.adapt_payment_details({"type": "VISA"})
101+
.adapt_payment_methods([{"type": "VISA"}])
102+
.get_dependencies()
103+
)
104+
assert "costs" in deps
105+
assert "custom_info" in deps
106+
assert "payment_details" in deps
107+
assert "payment_methods" in deps
108+
assert deps["status"] == "completed"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Unit tests for capture path package."""
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Unit tests for CaptureManager.capture_reservation_cancel behavior."""
9+
10+
from unittest.mock import MagicMock
11+
12+
from multisafepay.api.base.response.api_response import ApiResponse
13+
from multisafepay.api.base.response.custom_api_response import (
14+
CustomApiResponse,
15+
)
16+
from multisafepay.api.paths.capture.capture_manager import CaptureManager
17+
from multisafepay.api.paths.capture.request.capture_request import (
18+
CaptureRequest,
19+
)
20+
from multisafepay.api.paths.capture.response.capture import CancelReservation
21+
22+
ORDER_ID = "capture-order-1"
23+
24+
25+
def _build_capture_api_response() -> ApiResponse:
26+
return ApiResponse(
27+
headers={},
28+
status_code=200,
29+
body={
30+
"success": True,
31+
"data": {
32+
"order_id": ORDER_ID,
33+
"success": True,
34+
"transaction_id": "txn-001",
35+
},
36+
},
37+
)
38+
39+
40+
def _build_empty_capture_response() -> ApiResponse:
41+
return ApiResponse(
42+
headers={},
43+
status_code=200,
44+
body={"success": True, "data": {}},
45+
)
46+
47+
48+
def test_capture_reservation_cancel_sends_patch_and_parses() -> None:
49+
"""Send PATCH to json/capture/{order_id} and parse CancelReservation."""
50+
client = MagicMock()
51+
client.create_patch_request.return_value = _build_capture_api_response()
52+
53+
request = CaptureRequest(status="cancelled", reason="test")
54+
55+
manager = CaptureManager(client)
56+
response = manager.capture_reservation_cancel(
57+
order_id=ORDER_ID,
58+
capture_request=request,
59+
)
60+
61+
called_endpoint = client.create_patch_request.call_args.args[0]
62+
63+
assert isinstance(response, CustomApiResponse)
64+
assert isinstance(response.get_data(), CancelReservation)
65+
assert response.get_data().order_id == ORDER_ID
66+
assert f"json/capture/{ORDER_ID}" == called_endpoint
67+
68+
69+
def test_capture_reservation_cancel_empty_data() -> None:
70+
"""Return None data when body data is empty."""
71+
client = MagicMock()
72+
client.create_patch_request.return_value = _build_empty_capture_response()
73+
74+
request = CaptureRequest(status="cancelled", reason="test")
75+
76+
manager = CaptureManager(client)
77+
response = manager.capture_reservation_cancel(
78+
order_id=ORDER_ID,
79+
capture_request=request,
80+
)
81+
82+
assert isinstance(response, CustomApiResponse)
83+
assert response.get_data() is None
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright (c) MultiSafepay, Inc. All rights reserved.
2+
3+
# This file is licensed under the Open Software License (OSL) version 3.0.
4+
# For a copy of the license, see the LICENSE.txt file in the project root.
5+
6+
# See the DISCLAIMER.md file for disclaimer details.
7+
8+
"""Unit tests for OrderManager.cancel_transaction behavior."""
9+
10+
from unittest.mock import MagicMock
11+
12+
from multisafepay.api.base.response.api_response import ApiResponse
13+
from multisafepay.api.base.response.custom_api_response import (
14+
CustomApiResponse,
15+
)
16+
from multisafepay.api.paths.orders.order_id.cancel.request.cancel_transaction_request import (
17+
CancelTransactionRequest,
18+
)
19+
from multisafepay.api.paths.orders.order_id.cancel.response.cancel_transaction import (
20+
CancelTransaction,
21+
)
22+
from multisafepay.api.paths.orders.order_manager import OrderManager
23+
from multisafepay.client.client import Client
24+
from multisafepay.client.credential_resolver import AuthScope
25+
26+
ORDER_ID = "cloud-pos-cancel-1"
27+
TERMINAL_GROUP_ID = "Default"
28+
29+
30+
def _build_cancel_api_response() -> ApiResponse:
31+
return ApiResponse(
32+
headers={},
33+
status_code=200,
34+
body={
35+
"success": True,
36+
"data": {
37+
"status": "void",
38+
"financial_status": "void",
39+
"created": "2026-01-01T00:00:00",
40+
"modified": "2026-01-01T00:00:01",
41+
},
42+
},
43+
)
44+
45+
46+
def test_cancel_transaction_with_terminal_group_scope() -> None:
47+
"""Use terminal-group auth scope when terminal_group_id is provided."""
48+
client = MagicMock()
49+
client.create_post_request.return_value = _build_cancel_api_response()
50+
51+
manager = OrderManager(client)
52+
response = manager.cancel_transaction(
53+
cancel_transaction_request=ORDER_ID,
54+
terminal_group_id=TERMINAL_GROUP_ID,
55+
)
56+
57+
called_auth_scope = client.create_post_request.call_args.kwargs[
58+
"auth_scope"
59+
]
60+
61+
assert isinstance(response, CustomApiResponse)
62+
assert isinstance(response.get_data(), CancelTransaction)
63+
assert response.get_data().status == "void"
64+
assert called_auth_scope == AuthScope(
65+
scope=Client.AUTH_SCOPE_TERMINAL_GROUP,
66+
group_id=TERMINAL_GROUP_ID,
67+
)
68+
69+
70+
def test_cancel_transaction_without_terminal_group_scope() -> None:
71+
"""Omit auth scope when terminal_group_id is not provided."""
72+
client = MagicMock()
73+
client.create_post_request.return_value = _build_cancel_api_response()
74+
75+
manager = OrderManager(client)
76+
response = manager.cancel_transaction(
77+
cancel_transaction_request=ORDER_ID,
78+
)
79+
80+
called_auth_scope = client.create_post_request.call_args.kwargs[
81+
"auth_scope"
82+
]
83+
84+
assert isinstance(response, CustomApiResponse)
85+
assert called_auth_scope is None
86+
87+
88+
def test_cancel_transaction_accepts_request_object() -> None:
89+
"""Accept CancelTransactionRequest as input instead of raw string."""
90+
client = MagicMock()
91+
client.create_post_request.return_value = _build_cancel_api_response()
92+
93+
request = CancelTransactionRequest(order_id=ORDER_ID)
94+
95+
manager = OrderManager(client)
96+
response = manager.cancel_transaction(
97+
cancel_transaction_request=request,
98+
terminal_group_id=TERMINAL_GROUP_ID,
99+
)
100+
101+
called_endpoint = client.create_post_request.call_args.args[0]
102+
103+
assert isinstance(response, CustomApiResponse)
104+
assert ORDER_ID in called_endpoint
105+
assert called_endpoint.endswith("/cancel")
106+
107+
108+
def test_cancel_transaction_encodes_order_id() -> None:
109+
"""Verify order ID with special chars is encoded in the endpoint."""
110+
client = MagicMock()
111+
client.create_post_request.return_value = _build_cancel_api_response()
112+
113+
manager = OrderManager(client)
114+
manager.cancel_transaction(
115+
cancel_transaction_request="order/special&chars",
116+
)
117+
118+
called_endpoint = client.create_post_request.call_args.args[0]
119+
assert "order%2Fspecial%26chars" in called_endpoint

0 commit comments

Comments
 (0)