Skip to content

Commit 446bd2c

Browse files
authored
Pthmint 74 (#23)
1 parent 0a9b2fc commit 446bd2c

File tree

8 files changed

+205
-11
lines changed

8 files changed

+205
-11
lines changed

src/multisafepay/api/base/abstract_manager.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
# See the DISCLAIMER.md file for disclaimer details.
77

8+
import urllib.parse
89

910
from multisafepay.client.client import Client
1011

@@ -29,3 +30,19 @@ def __init__(self: "AbstractManager", client: Client) -> None:
2930
3031
"""
3132
self.client = client
33+
34+
@staticmethod
35+
def encode_path_segment(segment: str) -> str:
36+
"""
37+
URL encode a path segment to be safely included in a URL.
38+
39+
Parameters
40+
----------
41+
segment (str): The path segment to encode
42+
43+
Returns
44+
-------
45+
str: The URL encoded path segment
46+
47+
"""
48+
return urllib.parse.quote(str(segment), safe="")

src/multisafepay/api/paths/capture/capture_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ def capture_reservation_cancel(
5959
6060
"""
6161
json_data = json.dumps(capture_request.dict())
62+
encoded_order_id = self.encode_path_segment(order_id)
6263
response = self.client.create_patch_request(
63-
f"json/capture/{order_id}",
64+
f"json/capture/{encoded_order_id}",
6465
request_body=json_data,
6566
)
6667
args: dict = {

src/multisafepay/api/paths/gateways/gateway_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ def get_by_code(
102102
options = {}
103103
options = {k: v for k, v in options.items() if k in ALLOWED_OPTIONS}
104104

105+
encoded_gateway_code = self.encode_path_segment(gateway_code)
105106
response = self.client.create_get_request(
106-
f"json/gateways/{gateway_code}",
107+
f"json/gateways/{encoded_gateway_code}",
107108
options,
108109
)
109110
args: dict = {

src/multisafepay/api/paths/issuers/issuer_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ def get_issuers_by_gateway_code(
5959
if gateway_code not in ALLOWED_GATEWAY_CODES:
6060
raise InvalidArgumentException("Gateway code is not allowed")
6161

62+
encoded_gateway_code = self.encode_path_segment(gateway_code)
6263
response = self.client.create_get_request(
63-
f"json/issuers/{gateway_code}",
64+
f"json/issuers/{encoded_gateway_code}",
6465
)
6566
args: dict = {
6667
**response.dict(),

src/multisafepay/api/paths/orders/order_manager.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ def get(self: "OrderManager", order_id: str) -> CustomApiResponse:
102102
CustomApiResponse: The custom API response containing the order data.
103103
104104
"""
105-
endpoint = f"json/orders/{order_id}"
105+
encoded_order_id = self.encode_path_segment(order_id)
106+
endpoint = f"json/orders/{encoded_order_id}"
106107
context = {"order_id": order_id}
107108
response: ApiResponse = self.client.create_get_request(
108109
endpoint,
@@ -152,8 +153,9 @@ def update(
152153
153154
"""
154155
json_data = json.dumps(update_request.to_dict())
156+
encoded_order_id = self.encode_path_segment(order_id)
155157
response = self.client.create_patch_request(
156-
f"json/orders/{order_id}",
158+
f"json/orders/{encoded_order_id}",
157159
request_body=json_data,
158160
)
159161
args: dict = {
@@ -181,9 +183,10 @@ def capture(
181183
182184
"""
183185
json_data = json.dumps(capture_request.to_dict())
186+
encoded_order_id = self.encode_path_segment(order_id)
184187

185188
response = self.client.create_post_request(
186-
f"json/orders/{order_id}/capture",
189+
f"json/orders/{encoded_order_id}/capture",
187190
request_body=json_data,
188191
)
189192
args: dict = {
@@ -221,8 +224,9 @@ def refund(
221224
222225
"""
223226
json_data = json.dumps(request_refund.to_dict())
227+
encoded_order_id = self.encode_path_segment(order_id)
224228
response = self.client.create_post_request(
225-
f"json/orders/{order_id}/refunds",
229+
f"json/orders/{encoded_order_id}/refunds",
226230
request_body=json_data,
227231
)
228232
args: dict = {
@@ -267,6 +271,7 @@ def refund_by_item(
267271
quantity,
268272
)
269273

274+
# Encode the order_id before calling refund
270275
return self.refund(order.order_id, request_refund)
271276

272277
@staticmethod

src/multisafepay/api/paths/payment_methods/payment_method_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ def get_by_gateway_code(
126126
if options is None:
127127
options = {}
128128
options = {k: v for k, v in options.items() if k in ALLOWED_OPTIONS}
129+
encoded_gateway_code = self.encode_path_segment(gateway_code)
129130
response = self.client.create_get_request(
130-
f"json/payment-methods/{gateway_code}",
131+
f"json/payment-methods/{encoded_gateway_code}",
131132
options,
132133
)
133134
args: dict = {

src/multisafepay/api/paths/recurring/recurring_manager.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ def get_list(
6464
CustomApiResponse: The response containing the list of tokens.
6565
6666
"""
67+
encoded_reference = self.encode_path_segment(reference)
6768
response: ApiResponse = self.client.create_get_request(
68-
f"json/recurring/{reference}",
69+
f"json/recurring/{encoded_reference}",
6970
)
7071
args: dict = {
7172
**response.dict(),
@@ -109,8 +110,10 @@ def get(
109110
CustomApiResponse: The response containing the token data.
110111
111112
"""
113+
encoded_reference = self.encode_path_segment(reference)
114+
encoded_token = self.encode_path_segment(token)
112115
response = self.client.create_get_request(
113-
f"json/recurring/{reference}/token/{token}",
116+
f"json/recurring/{encoded_reference}/token/{encoded_token}",
114117
)
115118
args: dict = {
116119
**response.dict(),
@@ -144,8 +147,10 @@ def delete(
144147
CustomApiResponse: The response after deleting the token.
145148
146149
"""
150+
encoded_reference = self.encode_path_segment(reference)
151+
encoded_token = self.encode_path_segment(token)
147152
response = self.client.create_delete_request(
148-
f"json/recurring/{reference}/remove/{token}",
153+
f"json/recurring/{encoded_reference}/remove/{encoded_token}",
149154
)
150155
args: dict = {
151156
**response.dict(),
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
9+
from multisafepay.api.base.abstract_manager import AbstractManager
10+
11+
12+
def test_encode_path_segment_with_normal_string():
13+
"""Test encoding a normal string without special characters."""
14+
result = AbstractManager.encode_path_segment("normal_string")
15+
assert result == "normal_string"
16+
17+
18+
def test_encode_path_segment_with_spaces():
19+
"""Test encoding a string with spaces."""
20+
result = AbstractManager.encode_path_segment("hello world")
21+
assert result == "hello%20world"
22+
23+
24+
def test_encode_path_segment_with_special_characters():
25+
"""Test encoding a string with various special characters."""
26+
result = AbstractManager.encode_path_segment("hello@world#test")
27+
assert result == "hello%40world%23test"
28+
29+
30+
def test_encode_path_segment_with_forward_slash():
31+
"""Test encoding a string with forward slashes."""
32+
result = AbstractManager.encode_path_segment("path/to/resource")
33+
assert result == "path%2Fto%2Fresource"
34+
35+
36+
def test_encode_path_segment_with_question_mark():
37+
"""Test encoding a string with question marks."""
38+
result = AbstractManager.encode_path_segment("query?param=value")
39+
assert result == "query%3Fparam%3Dvalue"
40+
41+
42+
def test_encode_path_segment_with_ampersand():
43+
"""Test encoding a string with ampersands."""
44+
result = AbstractManager.encode_path_segment("param1&param2")
45+
assert result == "param1%26param2"
46+
47+
48+
def test_encode_path_segment_with_equals_sign():
49+
"""Test encoding a string with equals signs."""
50+
result = AbstractManager.encode_path_segment("key=value")
51+
assert result == "key%3Dvalue"
52+
53+
54+
def test_encode_path_segment_with_percentage_sign():
55+
"""Test encoding a string with percentage signs."""
56+
result = AbstractManager.encode_path_segment("discount%off")
57+
assert result == "discount%25off"
58+
59+
60+
def test_encode_path_segment_with_plus_sign():
61+
"""Test encoding a string with plus signs."""
62+
result = AbstractManager.encode_path_segment("one+two")
63+
assert result == "one%2Btwo"
64+
65+
66+
def test_encode_path_segment_with_unicode_characters():
67+
"""Test encoding a string with Unicode characters."""
68+
result = AbstractManager.encode_path_segment("café")
69+
assert result == "caf%C3%A9"
70+
71+
72+
def test_encode_path_segment_with_emoji():
73+
"""Test encoding a string with emoji characters."""
74+
result = AbstractManager.encode_path_segment("hello😊world")
75+
assert result == "hello%F0%9F%98%8Aworld"
76+
77+
78+
def test_encode_path_segment_with_empty_string():
79+
"""Test encoding an empty string."""
80+
result = AbstractManager.encode_path_segment("")
81+
assert result == ""
82+
83+
84+
def test_encode_path_segment_with_only_special_characters():
85+
"""Test encoding a string with only special characters."""
86+
result = AbstractManager.encode_path_segment("!@#$%^&*()")
87+
assert result == "%21%40%23%24%25%5E%26%2A%28%29"
88+
89+
90+
def test_encode_path_segment_with_numbers():
91+
"""Test encoding a string with numbers."""
92+
result = AbstractManager.encode_path_segment("123456")
93+
assert result == "123456"
94+
95+
96+
def test_encode_path_segment_with_mixed_alphanumeric():
97+
"""Test encoding a string with mixed alphanumeric characters."""
98+
result = AbstractManager.encode_path_segment("abc123XYZ")
99+
assert result == "abc123XYZ"
100+
101+
102+
def test_encode_path_segment_with_hyphen_and_underscore():
103+
"""Test encoding a string with hyphens and underscores (safe characters)."""
104+
result = AbstractManager.encode_path_segment("test-value_123")
105+
assert result == "test-value_123"
106+
107+
108+
def test_encode_path_segment_with_period_and_tilde():
109+
"""Test encoding a string with periods and tildes (safe characters)."""
110+
result = AbstractManager.encode_path_segment("file.txt~backup")
111+
assert result == "file.txt~backup"
112+
113+
114+
def test_encode_path_segment_with_integer_input():
115+
"""Test encoding an integer input (should be converted to string)."""
116+
result = AbstractManager.encode_path_segment(12345)
117+
assert result == "12345"
118+
119+
120+
def test_encode_path_segment_with_float_input():
121+
"""Test encoding a float input (should be converted to string)."""
122+
result = AbstractManager.encode_path_segment(123.45)
123+
assert result == "123.45"
124+
125+
126+
def test_encode_path_segment_with_none_input():
127+
"""Test encoding None input (should be converted to string)."""
128+
result = AbstractManager.encode_path_segment(None)
129+
assert result == "None"
130+
131+
132+
def test_encode_path_segment_preserves_unreserved_characters():
133+
"""Test that unreserved characters are not encoded."""
134+
# RFC 3986 unreserved characters: ALPHA / DIGIT / "-" / "." / "_" / "~"
135+
unreserved = (
136+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
137+
)
138+
result = AbstractManager.encode_path_segment(unreserved)
139+
assert result == unreserved
140+
141+
142+
def test_encode_path_segment_encodes_reserved_characters():
143+
"""Test that reserved characters are properly encoded."""
144+
# Some RFC 3986 reserved characters
145+
reserved = ":/?#[]@!$&'()*+,;="
146+
result = AbstractManager.encode_path_segment(reserved)
147+
# All characters should be encoded since safe="" is used
148+
assert ":" not in result
149+
assert "/" not in result
150+
assert "?" not in result
151+
assert "#" not in result
152+
assert "@" not in result
153+
assert "!" not in result
154+
assert "$" not in result
155+
assert "&" not in result
156+
assert "'" not in result
157+
assert "(" not in result
158+
assert ")" not in result
159+
assert "*" not in result
160+
assert "+" not in result
161+
assert "," not in result
162+
assert ";" not in result
163+
assert "=" not in result

0 commit comments

Comments
 (0)