Skip to content

Commit 64eb232

Browse files
committed
[client] add JWT auth support
1 parent c75b9a4 commit 64eb232

File tree

2 files changed

+122
-13
lines changed

2 files changed

+122
-13
lines changed

android_sms_gateway/client.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@
1515
class BaseClient(abc.ABC):
1616
def __init__(
1717
self,
18-
login: str,
19-
password: str,
18+
login: t.Optional[str],
19+
password_or_token: str,
2020
*,
2121
base_url: str = DEFAULT_URL,
2222
encryptor: t.Optional[BaseEncryptor] = None,
2323
) -> None:
24-
credentials = base64.b64encode(f"{login}:{password}".encode("utf-8")).decode(
25-
"utf-8"
26-
)
24+
if login and password_or_token:
25+
auth_header = f"Basic {base64.b64encode(f'{login}:{password_or_token}'.encode()).decode()}"
26+
elif password_or_token:
27+
auth_header = f"Bearer {password_or_token}"
28+
else:
29+
raise ValueError("Either login and password or token must be provided")
30+
2731
self.headers = {
28-
"Authorization": f"Basic {credentials}",
32+
"Authorization": auth_header,
2933
"Content-Type": "application/json",
3034
"User-Agent": f"android-sms-gateway/{VERSION} (client; python {sys.version_info.major}.{sys.version_info.minor})",
3135
}
@@ -87,14 +91,19 @@ def _decrypt(self, state: domain.MessageState) -> domain.MessageState:
8791
class APIClient(BaseClient):
8892
def __init__(
8993
self,
90-
login: str,
91-
password: str,
94+
login: t.Optional[str],
95+
password_or_token: str,
9296
*,
9397
base_url: str = DEFAULT_URL,
9498
encryptor: t.Optional[BaseEncryptor] = None,
9599
http: t.Optional[http.HttpClient] = None,
96100
) -> None:
97-
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
101+
super().__init__(
102+
login=login,
103+
password_or_token=password_or_token,
104+
base_url=base_url,
105+
encryptor=encryptor,
106+
)
98107
self.http = http
99108
self.default_http = None
100109

@@ -221,14 +230,19 @@ def health_check(self) -> dict:
221230
class AsyncAPIClient(BaseClient):
222231
def __init__(
223232
self,
224-
login: str,
225-
password: str,
233+
login: t.Optional[str],
234+
password_or_token: str,
226235
*,
227236
base_url: str = DEFAULT_URL,
228237
encryptor: t.Optional[BaseEncryptor] = None,
229238
http_client: t.Optional[ahttp.AsyncHttpClient] = None,
230239
) -> None:
231-
super().__init__(login, password, base_url=base_url, encryptor=encryptor)
240+
super().__init__(
241+
login=login,
242+
password_or_token=password_or_token,
243+
base_url=base_url,
244+
encryptor=encryptor,
245+
)
232246
self.http = http_client
233247
self.default_http = None
234248

@@ -244,7 +258,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
244258
if self.default_http is None:
245259
return
246260

247-
await self.default_http.__aexit__(exc_type, exc_val, exc_tb)
261+
self.default_http.__aexit__(exc_type, exc_val, exc_tb)
248262
self.http = self.default_http = None
249263

250264
async def send(self, message: domain.Message) -> domain.MessageState:

tests/test_jwt_auth.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import pytest
2+
from android_sms_gateway.client import APIClient, AsyncAPIClient
3+
from android_sms_gateway.constants import DEFAULT_URL
4+
from android_sms_gateway.http import RequestsHttpClient
5+
6+
7+
def test_basic_auth_initialization():
8+
"""Test that the client can be initialized with Basic Auth (backward compatibility)."""
9+
with (
10+
RequestsHttpClient() as h,
11+
APIClient(
12+
"test_login",
13+
"test_password",
14+
base_url=DEFAULT_URL,
15+
http=h,
16+
) as client,
17+
):
18+
# Check that the Authorization header is set correctly
19+
assert "Authorization" in client.headers
20+
assert client.headers["Authorization"].startswith("Basic ")
21+
assert client.headers["Content-Type"] == "application/json"
22+
assert "User-Agent" in client.headers
23+
24+
25+
def test_jwt_auth_initialization():
26+
"""Test that the client can be initialized with JWT token."""
27+
with (
28+
RequestsHttpClient() as h,
29+
APIClient(
30+
login=None,
31+
password_or_token="test_jwt_token",
32+
base_url=DEFAULT_URL,
33+
http=h,
34+
) as client,
35+
):
36+
# Check that the Authorization header is set correctly
37+
assert "Authorization" in client.headers
38+
assert client.headers["Authorization"] == "Bearer test_jwt_token"
39+
assert client.headers["Content-Type"] == "application/json"
40+
assert "User-Agent" in client.headers
41+
42+
43+
def test_async_basic_auth_initialization():
44+
"""Test that the async client can be initialized with Basic Auth (backward compatibility)."""
45+
client = AsyncAPIClient(
46+
"test_login",
47+
"test_password",
48+
base_url=DEFAULT_URL,
49+
)
50+
# Check that the Authorization header is set correctly
51+
assert "Authorization" in client.headers
52+
assert client.headers["Authorization"].startswith("Basic ")
53+
assert client.headers["Content-Type"] == "application/json"
54+
assert "User-Agent" in client.headers
55+
56+
57+
def test_async_jwt_auth_initialization():
58+
"""Test that the async client can be initialized with JWT token."""
59+
client = AsyncAPIClient(
60+
login=None,
61+
password_or_token="test_jwt_token",
62+
base_url=DEFAULT_URL,
63+
)
64+
# Check that the Authorization header is set correctly
65+
assert "Authorization" in client.headers
66+
assert client.headers["Authorization"] == "Bearer test_jwt_token"
67+
assert client.headers["Content-Type"] == "application/json"
68+
assert "User-Agent" in client.headers
69+
70+
71+
def test_missing_credentials_error():
72+
"""Test that an error is raised when neither login/password nor jwt_token is provided."""
73+
with pytest.raises(
74+
ValueError,
75+
match="Either login and password or token must be provided",
76+
):
77+
APIClient(None, "")
78+
79+
80+
def test_missing_password_error():
81+
"""Test that an error is raised when login is provided but password is missing."""
82+
with pytest.raises(
83+
ValueError,
84+
match="Either login and password or token must be provided",
85+
):
86+
APIClient(login="test_login", password_or_token="")
87+
88+
89+
def test_async_missing_credentials_error():
90+
"""Test that an error is raised when neither login/password nor jwt_token is provided for async client."""
91+
with pytest.raises(
92+
ValueError,
93+
match="Either login and password or token must be provided",
94+
):
95+
AsyncAPIClient(None, "")

0 commit comments

Comments
 (0)