Skip to content

Commit 27ed61f

Browse files
committedMay 5, 2023
update tests for python
1 parent 3bcfba7 commit 27ed61f

15 files changed

+692
-33
lines changed
 

‎.coveragerc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[run]
2+
omit =
3+
./nitric/proto/*

‎nitric/api/storage.py

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
StorageListFilesRequest,
3232
)
3333
from enum import Enum
34+
from warnings import warn
3435

3536

3637
class Storage(object):
@@ -140,6 +141,7 @@ async def download_url(self, expiry: int = 600):
140141

141142
async def sign_url(self, mode: FileMode = FileMode.READ, expiry: int = 3600):
142143
"""Generate a signed URL for reading or writing to a file."""
144+
warn("File.sign_url() is deprecated, use upload_url() or download_url() instead", DeprecationWarning)
143145
try:
144146
response = await self._storage._storage_stub.pre_sign_url(
145147
storage_pre_sign_url_request=StoragePreSignUrlRequest(

‎nitric/application.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ def _create_resource(cls, resource: Type[BT], name: str, *args, **kwargs) -> BT:
6767
)
6868

6969
@classmethod
70-
def _create_tracer(cls) -> TracerProvider:
71-
local_run = "OTELCOL_BIN" not in environ
72-
samplePercent = int(getenv("NITRIC_TRACE_SAMPLE_PERCENT", "100")) / 100.0
70+
def _create_tracer(cls, local: bool = True, sampler: int = 100) -> TracerProvider:
71+
local_run = local or "OTELCOL_BIN" not in environ
72+
samplePercent = int(getenv("NITRIC_TRACE_SAMPLE_PERCENT", sampler)) / 100.0
7373

7474
# If its a local run use a console exporter, otherwise export using OTEL Protocol
7575
exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)

‎nitric/faas.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,10 @@ class Frequency(Enum):
395395
@staticmethod
396396
def from_str(value: str) -> Frequency:
397397
"""Convert a string frequency value to a Frequency."""
398-
return Frequency[value.strip().lower()]
398+
try:
399+
return Frequency[value.strip().lower()]
400+
except Exception:
401+
raise ValueError(f"{value} is not valid frequency")
399402

400403
@staticmethod
401404
def as_str_list() -> List[str]:

‎nitric/resources/apis.py

+18-13
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ class ApiDetails:
5252

5353
@dataclass
5454
class JwtSecurityDefinition:
55-
"""Represents the JWT security definition for an API."""
55+
"""
56+
Represents the JWT security definition for an API.
57+
58+
issuer (str): the JWT issuer
59+
audiences (List[str]): a list of the allowed audiences for the API
60+
"""
5661

5762
issuer: str
5863
audiences: List[str]
@@ -68,16 +73,16 @@ class ApiOptions:
6873
"""Represents options when creating an API, such as middleware to be applied to all HTTP request to the API."""
6974

7075
path: str
71-
middleware: Union[HttpMiddleware, List[HttpMiddleware], None]
72-
security_definitions: Union[dict[str, SecurityDefinition], None]
73-
security: Union[dict[str, List[str]], None]
76+
middleware: Union[HttpMiddleware, List[HttpMiddleware]]
77+
security_definitions: dict[str, SecurityDefinition]
78+
security: dict[str, List[str]]
7479

7580
def __init__(
7681
self,
7782
path: str = "",
7883
middleware: List[Middleware] = [],
79-
security_definitions: dict[str, SecurityDefinition] = None,
80-
security: dict[str, List[str]] = None,
84+
security_definitions: dict[str, SecurityDefinition] = {},
85+
security: dict[str, List[str]] = {},
8186
):
8287
"""Construct a new API options object."""
8388
self.middleware = middleware
@@ -102,18 +107,18 @@ def _to_resource(b: Api) -> Resource:
102107

103108
def _security_definition_to_grpc_declaration(
104109
security_definitions: dict[str, SecurityDefinition]
105-
) -> Union[dict[str, ApiSecurityDefinition], None]:
110+
) -> dict[str, ApiSecurityDefinition]:
106111
if security_definitions is None or len(security_definitions) == 0:
107-
return None
112+
return {}
108113
return {
109114
k: ApiSecurityDefinition(jwt=ApiSecurityDefinitionJwt(issuer=v.issuer, audiences=v.audiences))
110115
for k, v in security_definitions.items()
111116
}
112117

113118

114-
def _security_to_grpc_declaration(security: dict[str, List[str]]) -> dict[str, ApiScopes] | None:
119+
def _security_to_grpc_declaration(security: dict[str, List[str]]) -> dict[str, ApiScopes]:
115120
if security is None or len(security) == 0:
116-
return None
121+
return {}
117122
return {k: ApiScopes(v) for k, v in security.items()}
118123

119124

@@ -187,7 +192,7 @@ def decorator(function: HttpMiddleware):
187192
return decorator
188193

189194
def methods(self, methods: List[HttpMethod], match: str, opts: MethodOptions = None):
190-
"""Define an HTTP route which will respond to HTTP GET requests."""
195+
"""Define an HTTP route which will respond to specific HTTP requests defined by a list of verbs."""
191196
if opts is None:
192197
opts = MethodOptions()
193198

@@ -275,7 +280,7 @@ async def _details(self) -> ApiDetails:
275280
except GRPCError as grpc_err:
276281
raise exception_from_grpc_error(grpc_err)
277282

278-
async def URL(self) -> str:
283+
async def url(self) -> str:
279284
"""Get the APIs live URL."""
280285
details = await self._details()
281286
return details.url
@@ -291,7 +296,7 @@ class Route:
291296
def __init__(self, api: Api, path: str, opts: RouteOptions):
292297
"""Define a route to be handled by the provided API."""
293298
self.api = api
294-
self.path = api.path.join(path)
299+
self.path = (api.path + path).replace("//", "/")
295300
self.middleware = opts.middleware if opts.middleware is not None else []
296301

297302
def method(self, methods: List[HttpMethod], *middleware: HttpMiddleware, opts: MethodOptions = None):

‎nitric/resources/buckets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def _register(self):
6262
except GRPCError as grpc_err:
6363
raise exception_from_grpc_error(grpc_err)
6464

65-
def _perms_to_actions(self, *args: [Union[BucketPermission, str]]) -> List[Action]:
65+
def _perms_to_actions(self, *args: List[Union[BucketPermission, str]]) -> List[Action]:
6666
permission_actions_map = {
6767
BucketPermission.reading: [Action.BucketFileGet, Action.BucketFileList],
6868
BucketPermission.writing: [Action.BucketFilePut],

‎nitric/resources/schedules.py

+6-8
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,15 @@ def every(self, rate_description: str, *middleware: EventMiddleware):
4747
# handle singular frequencies. e.g. every('day')
4848
rate_description = f"1 {rate_description}s" # 'day' becomes '1 days'
4949

50-
rate, freq_str = rate_description.split(" ")
51-
freq = Frequency.from_str(freq_str)
50+
try:
51+
rate, freq_str = rate_description.split(" ")
52+
freq = Frequency.from_str(freq_str)
53+
except Exception:
54+
raise Exception(f"invalid rate expression, frequency must be one of {Frequency.as_str_list()}")
5255

5356
if not rate.isdigit():
5457
raise Exception("invalid rate expression, expression must begin with a positive integer")
5558

56-
if not freq:
57-
raise Exception(
58-
f"invalid rate expression, frequency must be one of ${Frequency.as_str_list()}, received ${freq_str}"
59-
)
60-
6159
opts = RateWorkerOptions(self.description, int(rate), freq)
6260

6361
self.server = FunctionServer(opts)
@@ -73,4 +71,4 @@ def decorator(func: EventMiddleware):
7371
r.every(every, func)
7472
return r
7573

76-
return decorator
74+
return decorator

‎tests/api/test_documents.py

-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
DocumentQueryStreamRequest,
5151
)
5252
from nitric.proto.nitric.event.v1 import TopicListResponse, NitricTopic
53-
from nitric.utils import _struct_from_dict
5453

5554

5655
class Object(object):

‎tests/api/test_exception.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666

6767

6868
class TestException:
69-
@pytest.yield_fixture(autouse=True)
69+
@pytest.fixture(autouse=True)
7070
def init_exceptions(self):
7171
# Status codes that can be automatically converted to exceptions
7272
self.accepted_status_codes = set(_exception_code_map.keys())

‎tests/api/test_queues.py

+18
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@ async def test_send(self):
6363
)
6464
)
6565

66+
async def test_send_with_only_payload(self):
67+
mock_send = AsyncMock()
68+
mock_response = Object()
69+
mock_send.return_value = mock_response
70+
71+
payload = {"content": "of task"}
72+
73+
with patch("nitric.proto.nitric.queue.v1.QueueServiceStub.send", mock_send):
74+
queue = Queues().queue("test-queue")
75+
await queue.send(payload)
76+
77+
# Check expected values were passed to Stub
78+
mock_send.assert_called_once_with(
79+
queue_send_request=QueueSendRequest(
80+
queue="test-queue", task=NitricTask(id=None, payload_type=None, payload=_struct_from_dict(payload))
81+
)
82+
)
83+
6684
async def test_send_with_failed(self):
6785
payload = {"content": "of task"}
6886

‎tests/api/test_storage.py

+70-4
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,88 @@ async def test_delete(self):
9696
)
9797
)
9898

99-
async def test_sign_url(self):
99+
async def test_download_url_with_default_expiry(self):
100100
mock_pre_sign_url = AsyncMock()
101101
mock_pre_sign_url.return_value = StoragePreSignUrlResponse(url="www.example.com")
102102

103103
with patch("nitric.proto.nitric.storage.v1.StorageServiceStub.pre_sign_url", mock_pre_sign_url):
104104
bucket = Storage().bucket("test-bucket")
105105
file = bucket.file("test-file")
106-
url = await file.sign_url()
106+
url = await file.download_url()
107107

108108
# Check expected values were passed to Stub
109109
mock_pre_sign_url.assert_called_once_with(
110110
storage_pre_sign_url_request=StoragePreSignUrlRequest(
111111
bucket_name="test-bucket",
112112
key="test-file",
113113
operation=StoragePreSignUrlRequestOperation.READ,
114-
expiry=3600,
114+
expiry=600,
115+
)
116+
)
117+
118+
# check the URL is returned
119+
assert url == "www.example.com"
120+
121+
async def test_download_url_with_provided_expiry(self):
122+
mock_pre_sign_url = AsyncMock()
123+
mock_pre_sign_url.return_value = StoragePreSignUrlResponse(url="www.example.com")
124+
125+
with patch("nitric.proto.nitric.storage.v1.StorageServiceStub.pre_sign_url", mock_pre_sign_url):
126+
bucket = Storage().bucket("test-bucket")
127+
file = bucket.file("test-file")
128+
url = await file.download_url(60)
129+
130+
# Check expected values were passed to Stub
131+
mock_pre_sign_url.assert_called_once_with(
132+
storage_pre_sign_url_request=StoragePreSignUrlRequest(
133+
bucket_name="test-bucket",
134+
key="test-file",
135+
operation=StoragePreSignUrlRequestOperation.READ,
136+
expiry=60,
137+
)
138+
)
139+
140+
# check the URL is returned
141+
assert url == "www.example.com"
142+
143+
async def test_upload_url_with_default_expiry(self):
144+
mock_pre_sign_url = AsyncMock()
145+
mock_pre_sign_url.return_value = StoragePreSignUrlResponse(url="www.example.com")
146+
147+
with patch("nitric.proto.nitric.storage.v1.StorageServiceStub.pre_sign_url", mock_pre_sign_url):
148+
bucket = Storage().bucket("test-bucket")
149+
file = bucket.file("test-file")
150+
url = await file.upload_url()
151+
152+
# Check expected values were passed to Stub
153+
mock_pre_sign_url.assert_called_once_with(
154+
storage_pre_sign_url_request=StoragePreSignUrlRequest(
155+
bucket_name="test-bucket",
156+
key="test-file",
157+
operation=StoragePreSignUrlRequestOperation.WRITE,
158+
expiry=600,
159+
)
160+
)
161+
162+
# check the URL is returned
163+
assert url == "www.example.com"
164+
165+
async def test_upload_url_with_provided_expiry(self):
166+
mock_pre_sign_url = AsyncMock()
167+
mock_pre_sign_url.return_value = StoragePreSignUrlResponse(url="www.example.com")
168+
169+
with patch("nitric.proto.nitric.storage.v1.StorageServiceStub.pre_sign_url", mock_pre_sign_url):
170+
bucket = Storage().bucket("test-bucket")
171+
file = bucket.file("test-file")
172+
url = await file.upload_url(60)
173+
174+
# Check expected values were passed to Stub
175+
mock_pre_sign_url.assert_called_once_with(
176+
storage_pre_sign_url_request=StoragePreSignUrlRequest(
177+
bucket_name="test-bucket",
178+
key="test-file",
179+
operation=StoragePreSignUrlRequestOperation.WRITE,
180+
expiry=60,
115181
)
116182
)
117183

@@ -148,4 +214,4 @@ async def test_sign_url_error(self):
148214

149215
with patch("nitric.proto.nitric.storage.v1.StorageServiceStub.pre_sign_url", mock_pre_sign_url):
150216
with pytest.raises(UnknownException) as e:
151-
await Storage().bucket("test-bucket").file("test-file").sign_url()
217+
await Storage().bucket("test-bucket").file("test-file").upload_url()

‎tests/resources/test_apis.py

+343
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
#
2+
# Copyright (c) 2021 Nitric Technologies Pty Ltd.
3+
#
4+
# This file is part of Nitric Python 3 SDK.
5+
# See https://github.com/nitrictech/python-sdk for further info.
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
#
19+
from unittest import IsolatedAsyncioTestCase
20+
from unittest.mock import patch, AsyncMock, Mock
21+
22+
import pytest
23+
from grpclib import GRPCError, Status
24+
25+
from nitric.api.exception import InternalException
26+
from nitric.faas import HttpMethod, MethodOptions, ApiWorkerOptions, HttpContext, HttpRequest
27+
from nitric.proto.nitric.resource.v1 import (
28+
ResourceDeclareRequest,
29+
ApiResource,
30+
Resource,
31+
ResourceType,
32+
ResourceDetailsResponse,
33+
ApiResourceDetails,
34+
ResourceDetailsRequest,
35+
ApiScopes,
36+
ApiSecurityDefinition,
37+
ApiSecurityDefinitionJwt,
38+
)
39+
40+
from nitric.resources import api, ApiOptions, JwtSecurityDefinition
41+
from nitric.resources.apis import Method, Route, RouteOptions
42+
43+
44+
class Object(object):
45+
pass
46+
47+
48+
class ApiTest(IsolatedAsyncioTestCase):
49+
def test_create_default_api(self):
50+
mock_declare = AsyncMock()
51+
mock_response = Object()
52+
mock_declare.return_value = mock_response
53+
54+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
55+
api("test-api")
56+
57+
# Check expected values were passed to Stub
58+
mock_declare.assert_called_with(
59+
resource_declare_request=ResourceDeclareRequest(
60+
resource=Resource(type=ResourceType.Api, name="test-api"),
61+
api=ApiResource(security={}, security_definitions={}),
62+
)
63+
)
64+
65+
def test_create_api_throws_error(self):
66+
mock_declare = AsyncMock()
67+
mock_declare.side_effect = GRPCError(Status.INTERNAL, "test-error")
68+
69+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
70+
with pytest.raises(InternalException) as e:
71+
api("test-api-error")
72+
73+
def test_cached_api(self):
74+
mock_declare = AsyncMock()
75+
mock_response = Object()
76+
mock_declare.return_value = mock_response
77+
78+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
79+
api("test-api-cached")
80+
81+
api("test-api-cached")
82+
83+
# Check expected values were passed to Stub
84+
mock_declare.assert_called_once_with(
85+
resource_declare_request=ResourceDeclareRequest(
86+
resource=Resource(type=ResourceType.Api, name="test-api-cached"),
87+
api=ApiResource(security={}, security_definitions={}),
88+
)
89+
)
90+
91+
def test_create_api_with_empty_options(self):
92+
mock_declare = AsyncMock()
93+
mock_response = Object()
94+
mock_declare.return_value = mock_response
95+
96+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
97+
api("test-api-empty-options", ApiOptions())
98+
99+
# Check expected values were passed to Stub
100+
mock_declare.assert_called_with(
101+
resource_declare_request=ResourceDeclareRequest(
102+
resource=Resource(type=ResourceType.Api, name="test-api-empty-options"),
103+
api=ApiResource(security={}, security_definitions={}),
104+
)
105+
)
106+
107+
def test_create_api_with_base_path(self):
108+
mock_declare = AsyncMock()
109+
mock_response = Object()
110+
mock_declare.return_value = mock_response
111+
112+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
113+
test_api = api("test-api-base-path", ApiOptions(path="/api/v1"))
114+
115+
# Check expected values were passed to Stub
116+
mock_declare.assert_called_with(
117+
resource_declare_request=ResourceDeclareRequest(
118+
resource=Resource(type=ResourceType.Api, name="test-api-base-path"),
119+
api=ApiResource(security={}, security_definitions={}),
120+
)
121+
)
122+
123+
assert test_api.path == "/api/v1"
124+
125+
def test_create_api_with_security_definition(self):
126+
mock_declare = AsyncMock()
127+
mock_response = Object()
128+
mock_declare.return_value = mock_response
129+
130+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
131+
api(
132+
"test-api-security-definition",
133+
ApiOptions(
134+
security_definitions={
135+
"user": JwtSecurityDefinition(
136+
issuer="https://example-issuer.com", audiences=["test-audience", "other-audience"]
137+
)
138+
},
139+
security={"user": ["test:read", "test:write"]},
140+
),
141+
)
142+
143+
# Check expected values were passed to Stub
144+
mock_declare.assert_called_with(
145+
resource_declare_request=ResourceDeclareRequest(
146+
resource=Resource(type=ResourceType.Api, name="test-api-security-definition"),
147+
api=ApiResource(
148+
security_definitions={
149+
"user": ApiSecurityDefinition(
150+
jwt=ApiSecurityDefinitionJwt(
151+
issuer="https://example-issuer.com", audiences=["test-audience", "other-audience"]
152+
)
153+
)
154+
},
155+
security={"user": ApiScopes(scopes=["test:read", "test:write"])},
156+
),
157+
)
158+
)
159+
160+
async def test_get_api_url(self):
161+
mock_declare = AsyncMock()
162+
mock_response = Object()
163+
mock_declare.return_value = mock_response
164+
165+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
166+
test_api = api("test-api-get-url")
167+
168+
assert test_api is not None
169+
170+
# Test URL called
171+
mock_details = AsyncMock()
172+
mock_details.return_value = ResourceDetailsResponse(api=ApiResourceDetails(url="https://google-api.com/"))
173+
174+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.details", mock_details):
175+
url = await test_api.url()
176+
177+
assert url == "https://google-api.com/"
178+
179+
# Check expected values were passed to Stub
180+
mock_details.assert_called_once_with(
181+
resource_details_request=ResourceDetailsRequest(
182+
resource=Resource(type=ResourceType.Api, name="test-api-get-url")
183+
)
184+
)
185+
186+
async def test_get_api_url_throws_error(self):
187+
mock_declare = AsyncMock()
188+
mock_response = Object()
189+
mock_declare.return_value = mock_response
190+
191+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
192+
test_api = api("test-api-get-url")
193+
194+
assert test_api is not None
195+
196+
# Test URL called
197+
mock_details = AsyncMock()
198+
mock_details.side_effect = GRPCError(Status.INTERNAL, "test error")
199+
200+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.details", mock_details):
201+
with pytest.raises(InternalException) as e:
202+
await test_api.url()
203+
204+
def test_api_route(self):
205+
mock_declare = AsyncMock()
206+
mock_response = Object()
207+
mock_declare.return_value = mock_response
208+
209+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
210+
test_api = api("test-api-route", ApiOptions(path="/api/v2/"))
211+
212+
test_route = test_api._route("/hello")
213+
214+
assert test_route.path == "/api/v2/hello"
215+
assert test_route.middleware == []
216+
assert test_route.api.name == test_api.name
217+
218+
def test_define_route(self):
219+
mock_declare = AsyncMock()
220+
mock_response = Object()
221+
mock_declare.return_value = mock_response
222+
223+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
224+
test_api = api("test-api-define-route", ApiOptions(path="/api/v2/"))
225+
226+
test_route = Route(test_api, "/hello", opts=RouteOptions())
227+
228+
assert test_route.path == "/api/v2/hello"
229+
assert test_route.middleware == []
230+
assert test_route.api.name == test_api.name
231+
232+
def test_define_method(self):
233+
mock_declare = AsyncMock()
234+
mock_response = Object()
235+
mock_declare.return_value = mock_response
236+
237+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
238+
test_api = api("test-api-define-method", ApiOptions(path="/api/v2/"))
239+
240+
test_route = Route(test_api, "/hello", opts=RouteOptions())
241+
242+
test_method = Method(
243+
route=test_route,
244+
methods=[HttpMethod.GET, HttpMethod.POST],
245+
opts=MethodOptions(security={"user": ["test:delete"]}),
246+
)
247+
248+
assert test_method.methods == [HttpMethod.GET, HttpMethod.POST]
249+
assert test_method.route == test_route
250+
assert test_method.server is not None
251+
252+
assert isinstance(test_method.server._opts, ApiWorkerOptions)
253+
assert test_method.server._opts.methods == ["GET", "POST"]
254+
assert test_method.server._opts.api == "test-api-define-method"
255+
assert test_method.server._opts.opts.security == {"user": ["test:delete"]}
256+
257+
def test_api_get(self):
258+
mock_declare = AsyncMock()
259+
260+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
261+
test_api = api("test-api-get", ApiOptions(path="/api/v2/"))
262+
263+
test_api.get("/hello")(lambda ctx: ctx)
264+
265+
assert len(test_api.routes) == 1
266+
assert test_api.routes[0].path == "/api/v2/hello"
267+
268+
def test_api_post(self):
269+
mock_declare = AsyncMock()
270+
271+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
272+
test_api = api("test-api-post", ApiOptions(path="/api/v2/"))
273+
274+
test_api.post("/hello")(lambda ctx: ctx)
275+
276+
assert len(test_api.routes) == 1
277+
assert test_api.routes[0].path == "/api/v2/hello"
278+
279+
def test_api_delete(self):
280+
mock_declare = AsyncMock()
281+
282+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
283+
test_api = api("test-api-delete", ApiOptions(path="/api/v2/"))
284+
285+
test_api.delete("/hello")(lambda ctx: ctx)
286+
287+
assert len(test_api.routes) == 1
288+
assert test_api.routes[0].path == "/api/v2/hello"
289+
290+
def test_api_put(self):
291+
mock_declare = AsyncMock()
292+
293+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
294+
test_api = api("test-api-put", ApiOptions(path="/api/v2/"))
295+
296+
test_api.put("/hello")(lambda ctx: ctx)
297+
298+
assert len(test_api.routes) == 1
299+
assert test_api.routes[0].path == "/api/v2/hello"
300+
301+
def test_api_patch(self):
302+
mock_declare = AsyncMock()
303+
304+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
305+
test_api = api("test-api-patch", ApiOptions(path="/api/v2/"))
306+
307+
test_api.patch("/hello")(lambda ctx: ctx)
308+
309+
assert len(test_api.routes) == 1
310+
assert test_api.routes[0].path == "/api/v2/hello"
311+
312+
def test_api_all(self):
313+
mock_declare = AsyncMock()
314+
315+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
316+
test_api = api("test-api-all", ApiOptions(path="/api/v2/"))
317+
318+
test_api.all("/hello")(lambda ctx: ctx)
319+
320+
assert len(test_api.routes) == 1
321+
assert test_api.routes[0].path == "/api/v2/hello"
322+
323+
def test_api_methods(self):
324+
mock_declare = AsyncMock()
325+
326+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
327+
test_api = api("test-api-methods", ApiOptions(path="/api/v2/"))
328+
329+
test_api.methods([HttpMethod.GET], "/hello")(lambda ctx: ctx)
330+
331+
assert len(test_api.routes) == 1
332+
assert test_api.routes[0].path == "/api/v2/hello"
333+
334+
def test_api_options(self):
335+
mock_declare = AsyncMock()
336+
337+
with patch("nitric.proto.nitric.resource.v1.ResourceServiceStub.declare", mock_declare):
338+
test_api = api("test-api-options", ApiOptions(path="/api/v2/"))
339+
340+
test_api.options("/hello")(lambda ctx: ctx)
341+
342+
assert len(test_api.routes) == 1
343+
assert test_api.routes[0].path == "/api/v2/hello"

‎tests/resources/test_schedules.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#
2+
# Copyright (c) 2021 Nitric Technologies Pty Ltd.
3+
#
4+
# This file is part of Nitric Python 3 SDK.
5+
# See https://github.com/nitrictech/python-sdk for further info.
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
#
19+
from unittest import IsolatedAsyncioTestCase
20+
from unittest.mock import patch, AsyncMock, Mock
21+
22+
import pytest
23+
24+
from nitric.faas import RateWorkerOptions, Frequency
25+
from nitric.resources import Schedule, schedule
26+
27+
28+
class Object(object):
29+
pass
30+
31+
32+
class ApiTest(IsolatedAsyncioTestCase):
33+
def test_create_schedule(self):
34+
test_schedule = Schedule("test-schedule")
35+
36+
assert test_schedule is not None
37+
assert test_schedule.description == "test-schedule"
38+
39+
def test_create_schedule_decorator(self):
40+
test_schedule = schedule("test-schedule", "3 hours")(lambda ctx: ctx)
41+
42+
assert test_schedule is not None
43+
assert test_schedule.description == "test-schedule"
44+
assert isinstance(test_schedule.server._opts, RateWorkerOptions)
45+
assert test_schedule.server._opts.description == "test-schedule"
46+
assert test_schedule.server._opts.rate == 3
47+
assert test_schedule.server._opts.frequency == Frequency.hours
48+
49+
def test_valid_every(self):
50+
test_schedule = Schedule("test-schedule")
51+
52+
test_schedule.every("3 hours", lambda ctx: ctx)
53+
54+
assert test_schedule.server is not None
55+
assert isinstance(test_schedule.server._opts, RateWorkerOptions)
56+
assert test_schedule.server._opts.description == "test-schedule"
57+
assert test_schedule.server._opts.rate == 3
58+
assert test_schedule.server._opts.frequency == Frequency.hours
59+
60+
def test_every_with_invalid_rate_description_frequency(self):
61+
test_schedule = Schedule("test-schedule")
62+
63+
try:
64+
test_schedule.every("3 months", lambda ctx: ctx)
65+
pytest.fail()
66+
except Exception as e:
67+
assert str(e).startswith("invalid rate expression, frequency") is True
68+
69+
def test_every_with_missing_rate_description_frequency(self):
70+
test_schedule = Schedule("test-schedule")
71+
72+
try:
73+
test_schedule.every("3", lambda ctx: ctx)
74+
pytest.fail()
75+
except Exception as e:
76+
assert str(e).startswith("invalid rate expression, frequency") is True
77+
78+
def test_every_with_invalid_rate_description_rate(self):
79+
test_schedule = Schedule("test-schedule")
80+
81+
try:
82+
test_schedule.every("three days", lambda ctx: ctx)
83+
pytest.fail()
84+
except Exception as e:
85+
assert str(e).startswith("invalid rate expression, expression") is True
86+
87+
def test_every_with_invalid_rate_description_frequency_and_rate(self):
88+
test_schedule = Schedule("test-schedule")
89+
90+
try:
91+
test_schedule.every("three days", lambda ctx: ctx)
92+
pytest.fail()
93+
except Exception as e:
94+
assert str(e).startswith("invalid rate expression, expression") is True
95+
96+
def test_every_with_missing_rate_description_rate(self):
97+
test_schedule = Schedule("test-schedule")
98+
99+
try:
100+
test_schedule.every("months", lambda ctx: ctx)
101+
pytest.fail()
102+
except Exception as e:
103+
assert str(e).startswith("invalid rate expression, frequency") is True

‎tests/test_application.py

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#
2+
# Copyright (c) 2021 Nitric Technologies Pty Ltd.
3+
#
4+
# This file is part of Nitric Python 3 SDK.
5+
# See https://github.com/nitrictech/python-sdk for further info.
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
#
19+
from unittest import IsolatedAsyncioTestCase
20+
from unittest.mock import patch, AsyncMock, Mock, call
21+
22+
from grpclib import GRPCError, Status
23+
from opentelemetry.sdk.trace import TracerProvider, sampling
24+
25+
26+
from nitric.api.exception import NitricUnavailableException
27+
from nitric.resources import Bucket
28+
from nitric.application import Nitric
29+
30+
31+
class Object(object):
32+
pass
33+
34+
35+
class MockAsyncChannel:
36+
def __init__(self):
37+
self.send = AsyncMock()
38+
self.close = Mock()
39+
self.done = Mock()
40+
41+
42+
class ApplicationTest(IsolatedAsyncioTestCase):
43+
def test_create_resource(self):
44+
application = Nitric()
45+
mock_make = Mock()
46+
mock_make.side_effect = ConnectionRefusedError("test error")
47+
48+
with patch("nitric.resources.base.BaseResource.make", mock_make):
49+
try:
50+
application._create_resource(Bucket, "test-bucket")
51+
except NitricUnavailableException as e:
52+
assert str(e).startswith("Unable to connect")
53+
54+
def test_create_tracer(self):
55+
application = Nitric()
56+
57+
tracer = application._create_tracer(local=True, sampler=80)
58+
59+
assert tracer is not None
60+
assert isinstance(tracer.sampler, sampling.TraceIdRatioBased)
61+
assert tracer.sampler.rate == 0.8
62+
63+
def test_run(self):
64+
application = Nitric()
65+
66+
mock_running_loop = Mock()
67+
mock_event_loop = Mock()
68+
69+
with (patch("asyncio.get_event_loop", mock_event_loop), patch("asyncio.get_running_loop", mock_running_loop)):
70+
application.run()
71+
72+
mock_running_loop.assert_called_once()
73+
mock_event_loop.assert_not_called()
74+
75+
def test_run_with_no_active_event_loop(self):
76+
application = Nitric()
77+
78+
mock_running_loop = Mock()
79+
mock_running_loop.side_effect = RuntimeError("loop is not running")
80+
81+
mock_event_loop = Mock()
82+
83+
with (patch("asyncio.get_event_loop", mock_event_loop), patch("asyncio.get_running_loop", mock_running_loop)):
84+
application.run()
85+
86+
mock_running_loop.assert_called_once()
87+
mock_event_loop.assert_called_once()
88+
89+
def test_run_with_keyboard_interrupt(self):
90+
application = Nitric()
91+
92+
mock_running_loop = Mock()
93+
mock_running_loop.side_effect = KeyboardInterrupt("cancel")
94+
95+
mock_event_loop = Mock()
96+
97+
with (patch("asyncio.get_event_loop", mock_event_loop), patch("asyncio.get_running_loop", mock_running_loop)):
98+
application.run()
99+
100+
mock_running_loop.assert_called_once()
101+
mock_event_loop.assert_not_called()
102+
103+
def test_run_with_connection_refused(self):
104+
application = Nitric()
105+
106+
mock_running_loop = Mock()
107+
mock_running_loop.side_effect = ConnectionRefusedError("refusing connection")
108+
109+
mock_event_loop = Mock()
110+
111+
with (patch("asyncio.get_event_loop", mock_event_loop), patch("asyncio.get_running_loop", mock_running_loop)):
112+
try:
113+
application.run()
114+
pytest.fail()
115+
except NitricUnavailableException as e:
116+
assert str(e).startswith("Unable to connect to a nitric server!")
117+
118+
mock_running_loop.assert_called_once()
119+
mock_event_loop.assert_not_called()

‎tests/test_faas.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(self):
4545
self.done = Mock()
4646

4747

48-
class EventClientTest(IsolatedAsyncioTestCase):
48+
class FaasClientTest(IsolatedAsyncioTestCase):
4949
async def test_compose_middleware(self):
5050
async def middleware(ctx: HttpContext, next) -> HttpContext:
5151
ctx.res.status = 401

0 commit comments

Comments
 (0)
Please sign in to comment.