Skip to content

Commit e2e3315

Browse files
authored
improve testing, logging, transport and exceptions (#56)
1 parent d29b51f commit e2e3315

File tree

17 files changed

+338
-183
lines changed

17 files changed

+338
-183
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ test: clean
3737
test-cov: clean
3838
pytest --cov=ucloud/core tests
3939

40+
test-cov-html:
41+
pytest --cov=ucloud/core tests --cov-report html
42+
$(BROWSER) htmlcov/index.html
43+
4044
test-acc: clean
4145
USDKACC=1 pytest --cov=ucloud
4246

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ This client can run on Linux, macOS and Windows.
1717

1818
- Website: https://www.ucloud.cn/
1919
- Free software: Apache 2.0 license
20-
- [Documentation](https://ucloud.github.io/ucloud-sdk-python3/)
20+
- [Documentation](https://docs.ucloud.cn/opensdk-python/)

examples/auth/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# UCloud SDK Auth Example
2+
3+
## What is the goal
4+
5+
Create signature from request payload.
6+
7+
## Setup Environment
8+
9+
Don't need.
10+
11+
## How to run
12+
13+
```sh
14+
python main.py
15+
```

examples/auth/__init__.py

Whitespace-only changes.

examples/auth/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from ucloud.core import auth
2+
3+
4+
def main():
5+
cred = auth.Credential(
6+
"ucloudsomeone@example.com1296235120854146120",
7+
"46f09bb9fab4f12dfc160dae12273d5332b5debe",
8+
)
9+
d = {'Action': 'DescribeUHostInstance', 'Region': 'cn-bj2', 'Limit': 10}
10+
print(cred.verify_ac(d))
11+
12+
13+
if __name__ == '__main__':
14+
main()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def load_requirements(requirements_file):
5959

6060
dependencies = load_requirements("requirements.txt")
6161

62-
dependencies_test = dependencies + ["flake8>=3.6.0", "pytest", "pytest-cov"]
62+
dependencies_test = dependencies + ["flake8>=3.6.0", "pytest>=4.6", "pytest-cov", "requests_mock"]
6363

6464
dependencies_doc = dependencies + ["sphinx"]
6565

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TEST_URL = "https://api.ucloud.cn/"

tests/test_unit/test_core/test_auth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ def test_verify_ac():
2222
"46f09bb9fab4f12dfc160dae12273d5332b5debe",
2323
)
2424
assert cred.verify_ac(d) == "4f9ef5df2abab2c6fccd1e9515cb7e2df8c6bb65"
25+
assert cred.to_dict() == {
26+
"public_key": "ucloudsomeone@example.com1296235120854146120",
27+
"private_key": "46f09bb9fab4f12dfc160dae12273d5332b5debe",
28+
}
Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
import os
1+
import json
2+
import uuid
3+
24
import pytest
35
import logging
6+
import collections
7+
import requests_mock
48

59
from ucloud.client import Client
610
from ucloud.core import exc
11+
from ucloud.core.transport import RequestsTransport, http
712
from ucloud.testing.mock import MockedTransport
813

14+
from tests.test_unit.test_core import consts
15+
916
logger = logging.getLogger(__name__)
1017

1118

12-
@pytest.fixture(scope="session", autouse=True)
19+
@pytest.fixture(scope="function", autouse=True)
1320
def client():
1421
return Client(
1522
{
@@ -28,54 +35,61 @@ def transport():
2835
return MockedTransport()
2936

3037

31-
class TestClient:
32-
def test_client_invoke(self, client, transport):
33-
transport.mock_data(lambda _: {"RetCode": 0, "Action": "Foo"})
34-
client.transport = transport
38+
def test_client_invoke(client):
39+
expected = {"RetCode": 0, "Action": "Foo"}
40+
with requests_mock.Mocker() as m:
41+
m.post(consts.TEST_URL, text=json.dumps(expected), headers={http.REQUEST_UUID_HEADER_KEY: str(uuid.uuid4())})
42+
assert client.invoke("Foo") == expected
43+
3544

36-
assert client.invoke("Foo") == {"RetCode": 0, "Action": "Foo"}
45+
def test_client_invoke_code_error(client):
46+
expected = {"RetCode": 171, "Action": "Foo", "Message": "签名错误"}
3747

38-
def test_client_invoke_code_error(self, client, transport):
39-
transport.mock_data(lambda _: {"RetCode": 171, "Action": "Foo"})
40-
client.transport = transport
48+
with requests_mock.Mocker() as m:
49+
m.post(consts.TEST_URL, text=json.dumps(expected), headers={http.REQUEST_UUID_HEADER_KEY: str(uuid.uuid4())})
4150

4251
with pytest.raises(exc.RetCodeException):
4352
try:
4453
client.invoke("Foo")
4554
except exc.RetCodeException as e:
46-
assert str(e)
47-
expected = {"RetCode": 171, "Action": "Foo", "Message": ""}
48-
assert e.json() == expected
55+
assert e.retryable is False
56+
assert e.json() == {"RetCode": 171, "Action": "Foo", "Message": "签名错误"}
4957
raise e
5058

51-
def test_client_invoke_with_retryable_error(self, client, transport):
52-
# RetCodeError is retryable when code is greater than 2000
53-
transport.mock_data(lambda _: {"RetCode": 10000, "Action": "Foo"})
54-
client.transport = transport
5559

60+
def test_client_invoke_with_retryable_error(client):
61+
# RetCodeError is retryable when code is greater than 2000
62+
with requests_mock.Mocker() as m:
63+
m.post(
64+
consts.TEST_URL,
65+
text=json.dumps({"RetCode": 10000, "Action": "Foo"}),
66+
)
5667
with pytest.raises(exc.RetCodeException):
5768
client.invoke("Foo")
5869

59-
def test_client_invoke_with_unexpected_error(self, client, transport):
60-
def raise_error(_):
61-
raise ValueError("temporary error")
6270

63-
transport.mock_data(raise_error)
64-
client.transport = transport
71+
def test_client_invoke_with_unexpected_error(client):
72+
def raise_error(_):
73+
raise ValueError("temporary error")
6574

66-
with pytest.raises(ValueError):
67-
client.invoke("Foo")
75+
transport = RequestsTransport()
76+
transport.middleware.request(raise_error)
77+
client.transport = transport
78+
79+
with pytest.raises(ValueError):
80+
client.invoke("Foo")
81+
82+
83+
def test_client_try_import(client):
84+
for name in dir(client):
85+
if name.startswith("_") or name in [
86+
"invoke",
87+
"logged_request_handler",
88+
"logged_response_handler",
89+
"logged_exception_handler",
90+
]:
91+
continue
6892

69-
def test_client_try_import(self, client):
70-
assert client.pathx()
71-
assert client.stepflow()
72-
assert client.uaccount()
73-
assert client.udb()
74-
assert client.udpn()
75-
assert client.udisk()
76-
assert client.uhost()
77-
assert client.ulb()
78-
assert client.umem()
79-
assert client.unet()
80-
assert client.uphost()
81-
assert client.vpc()
93+
client_factory = getattr(client, name)
94+
if isinstance(client_factory, collections.Callable):
95+
print(client_factory())
Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,126 @@
1+
import json
2+
import uuid
3+
14
import pytest
25
import logging
6+
import requests_mock
7+
from collections import Counter
38

4-
from ucloud.core.transport import RequestsTransport, Request, Response, utils
9+
from tests.test_unit.test_core.consts import TEST_URL
10+
from ucloud.core import exc
11+
from ucloud.core.transport import RequestsTransport, Request, Response, utils, http
512

613
logger = logging.getLogger(__name__)
714

815

9-
@pytest.fixture(scope="function", autouse=True)
10-
def transport():
16+
@pytest.fixture(name="transport", scope="function", autouse=True)
17+
def transport_factory():
1118
return RequestsTransport()
1219

1320

14-
class TestTransport:
15-
def test_transport_send(self, transport):
16-
req = Request(
17-
url="http://httpbin.org/anything",
18-
method="post",
19-
json={"foo": "bar"},
20-
)
21-
resp = transport.send(req)
22-
assert resp.text
23-
assert resp.json()["json"] == {"foo": "bar"}
24-
25-
def test_transport_handler(self, transport):
26-
global_env = {}
27-
28-
def request_handler(r):
29-
global_env["req"] = r
30-
return r
31-
32-
def response_handler(r):
33-
global_env["resp"] = r
34-
return r
35-
36-
transport.middleware.request(handler=request_handler)
37-
transport.middleware.response(handler=response_handler)
38-
39-
req = Request(
40-
url="http://httpbin.org/anything",
41-
method="post",
42-
json={"foo": "bar"},
43-
)
21+
@pytest.mark.parametrize(
22+
argnames=("status_code", "content", "expect", "expect_exc", "retryable"),
23+
argvalues=(
24+
(
25+
200,
26+
'{"Action": "Mock", "RetCode": 0}',
27+
{"Action": "Mock", "RetCode": 0},
28+
None,
29+
False,
30+
),
31+
(500, "{}", None, exc.HTTPStatusException, False),
32+
(429, "{}", None, exc.HTTPStatusException, True),
33+
(500, "x", None, exc.HTTPStatusException, False),
34+
(200, "x", None, exc.InvalidResponseException, False),
35+
),
36+
)
37+
def test_transport(
38+
transport, status_code, content, expect, expect_exc, retryable
39+
):
40+
with requests_mock.Mocker() as m:
41+
m.post(TEST_URL, text=content, status_code=status_code)
42+
43+
got_exc = None
44+
try:
45+
resp = transport.send(Request(url=TEST_URL, method="post", json={}))
46+
assert resp.json() == expect
47+
except Exception as e:
48+
got_exc = e
49+
50+
if expect_exc:
51+
assert str(got_exc)
52+
assert got_exc.retryable == retryable
53+
assert isinstance(got_exc, expect_exc)
54+
55+
56+
def test_transport_handler(transport):
57+
req_key, resp_key, exc_key = "req", "resp", "exc"
58+
counter = Counter({req_key: 0, resp_key: 0, exc_key: 0})
59+
60+
def request_handler(r):
61+
counter[req_key] += 1
62+
return r
63+
64+
def response_handler(r):
65+
counter[resp_key] += 1
66+
return r
67+
68+
def exception_handler(r):
69+
counter[exc_key] += 1
70+
return r
71+
72+
transport.middleware.request(handler=request_handler)
73+
transport.middleware.response(handler=response_handler)
74+
transport.middleware.exception(handler=exception_handler)
75+
76+
expect = {"foo": "bar"}
77+
req = Request(url=TEST_URL, method="post", json=expect)
78+
79+
with requests_mock.Mocker() as m:
80+
request_uuid = str(uuid.uuid4())
81+
m.post(TEST_URL, text=json.dumps(expect), status_code=200,
82+
headers={http.REQUEST_UUID_HEADER_KEY: request_uuid})
4483
resp = transport.send(req)
4584
assert resp.text
46-
assert resp.json()["json"] == {"foo": "bar"}
47-
48-
assert "req" in global_env
49-
assert "resp" in global_env
50-
51-
52-
class TestResponse:
53-
def test_guess_json_utf(self):
54-
import json
55-
56-
encodings = [
57-
"utf-32",
58-
"utf-8-sig",
59-
"utf-16",
60-
"utf-8",
61-
"utf-16-be",
62-
"utf-16-le",
63-
"utf-32-be",
64-
"utf-32-le",
65-
]
66-
for e in encodings:
67-
s = json.dumps("表意字符").encode(e)
68-
assert utils.guess_json_utf(s) == e
69-
70-
def test_response_empty_content(self):
71-
r = Response("http://foo.bar", "post")
72-
assert not r.text
85+
assert resp.json() == expect
86+
assert resp.request_uuid == request_uuid
87+
88+
with pytest.raises(Exception):
89+
transport.send(Request(url="/"))
90+
91+
assert counter[req_key] == 2
92+
assert counter[resp_key] == 1
93+
assert counter[exc_key] == 1
94+
95+
96+
def test_guess_json_utf():
97+
encodings = [
98+
"utf-32",
99+
"utf-8-sig",
100+
"utf-16",
101+
"utf-8",
102+
"utf-16-be",
103+
"utf-16-le",
104+
"utf-32-be",
105+
"utf-32-le",
106+
]
107+
for e in encodings:
108+
s = json.dumps("表意字符").encode(e)
109+
assert utils.guess_json_utf(s) == e
110+
111+
112+
def test_request_methods():
113+
req = Request(
114+
TEST_URL, data={"foo": 42}, json={"bar": 42}, params={"q": "search"}
115+
)
116+
assert req.payload() == {"foo": 42, "bar": 42, "q": "search"}
117+
118+
119+
def test_response_methods():
120+
r = Response(TEST_URL, "post")
121+
assert not r.text
122+
assert r.json() is None
123+
124+
r = Response(TEST_URL, "post", content=b"\xd6", encoding="utf-8")
125+
with pytest.raises(exc.InvalidResponseException):
73126
assert r.json() is None

0 commit comments

Comments
 (0)