Skip to content

Commit aa80663

Browse files
committed
IDEV-2299: Add tests
1 parent 341db56 commit aa80663

File tree

2 files changed

+445
-0
lines changed

2 files changed

+445
-0
lines changed

tests/test_decorators.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import pytest
2+
from unittest.mock import Mock, patch
3+
from domaintools.decorators import api_endpoint
4+
5+
6+
@pytest.fixture
7+
def api_specs():
8+
"""
9+
A fixture that acts as a central registry for all test OpenAPI specs.
10+
"""
11+
return {
12+
# --- Spec V1: Standard ---
13+
"v1": {
14+
"openapi": "3.0.0",
15+
"info": {"title": "Standard Spec", "version": "1.0.0"},
16+
"paths": {
17+
"/users": {
18+
"get": {
19+
"parameters": [
20+
{
21+
"name": "status",
22+
"in": "query",
23+
"required": True,
24+
"schema": {"type": "string"},
25+
}
26+
]
27+
},
28+
"post": {
29+
"requestBody": {
30+
"required": True,
31+
"content": {
32+
"application/json": {
33+
"schema": {
34+
"type": "object",
35+
"properties": {
36+
"name": {"type": "string"},
37+
"age": {"type": "integer"},
38+
},
39+
}
40+
}
41+
},
42+
}
43+
},
44+
}
45+
},
46+
},
47+
# --- Spec V3: Complex Lookup (Matches parameter name in components) ---
48+
"v3_complex": {
49+
"openapi": "3.0.0",
50+
"info": {"title": "Complex Lookup Spec", "version": "3.0.0"},
51+
"components": {
52+
"parameters": {
53+
"LimitParam": {
54+
"name": "limit",
55+
"in": "query",
56+
"description": "Max number of items.",
57+
"schema": {"type": "integer"},
58+
}
59+
},
60+
"schemas": {
61+
"UserRequestParameters": {
62+
"type": "object",
63+
"properties": {
64+
"limit": {
65+
# The validator/patcher should match this name to 'LimitParam' above
66+
"$ref:": "#/components/schemas/IgnoredRef",
67+
},
68+
},
69+
},
70+
},
71+
"requestBodies": {
72+
"UserBody": {
73+
"required": True,
74+
"content": {
75+
"application/json": {
76+
"schema": {"$ref": "#/components/schemas/UserRequestParameters"},
77+
},
78+
},
79+
}
80+
},
81+
},
82+
"paths": {
83+
"/users": {
84+
"post": {
85+
"requestBody": {"$ref": "#/components/requestBodies/UserBody"},
86+
},
87+
},
88+
},
89+
},
90+
}
91+
92+
93+
@pytest.fixture
94+
def mock_client(api_specs):
95+
"""
96+
Creates a mock API client that is pre-patched with the specs defined above.
97+
"""
98+
client = Mock()
99+
client.specs = api_specs
100+
return client
101+
102+
103+
class TestApiEndpointDecorator:
104+
105+
def test_metadata_preservation(self, mock_client):
106+
"""
107+
Ensure decorator copies metadata for DocstringPatcher.
108+
"""
109+
110+
@api_endpoint(spec_name="v1", path="/users", methods="GET")
111+
def get_users():
112+
"""Original Docstring"""
113+
pass
114+
115+
bound_method = get_users.__get__(mock_client, Mock)
116+
117+
assert bound_method._api_spec_name == "v1"
118+
assert bound_method._api_path == "/users"
119+
assert bound_method._api_methods == ["GET"]
120+
assert bound_method.__doc__ == "Original Docstring"
121+
122+
def test_valid_post_request(self, mock_client):
123+
"""
124+
Test a valid POST request against 'v1' spec.
125+
"""
126+
127+
@api_endpoint(spec_name="v1", path="/users", methods="POST")
128+
def create_user(request_body=None):
129+
return "Created"
130+
131+
# Mocking validate to ensure arguments are passed correctly,
132+
# but we could also let it run against the real logic if we wanted integration tests.
133+
with patch("domaintools.request_validator.RequestValidator.validate") as mock_validate:
134+
result = create_user(mock_client, request_body={"name": "Alice", "age": 30})
135+
136+
assert result == "Created"
137+
138+
# Check arguments passed to validator
139+
call_kwargs = mock_validate.call_args[1]
140+
assert call_kwargs["spec"] == mock_client.specs["v1"]
141+
assert call_kwargs.get("parameters", {}).get("request_body") == {
142+
"name": "Alice",
143+
"age": 30,
144+
}
145+
146+
def test_validation_failure_blocks_execution(self, mock_client):
147+
"""
148+
Test that if validation fails, the function doesn't run.
149+
"""
150+
inner_logic = Mock()
151+
152+
@api_endpoint(spec_name="v1", path="/users", methods="POST")
153+
def create_user(body=None):
154+
inner_logic()
155+
156+
# Simulate a validation error
157+
with patch(
158+
"domaintools.request_validator.RequestValidator.validate",
159+
side_effect=ValueError("Bad Input"),
160+
):
161+
with pytest.raises(ValueError, match="Bad Input"):
162+
create_user(mock_client, body={"bad": "data"})
163+
164+
inner_logic.assert_not_called()
165+
166+
def test_complex_spec_lookup_integration(self, mock_client):
167+
"""
168+
Test that the decorator works with the complex 'v3_complex' spec
169+
we defined in the fixture.
170+
"""
171+
172+
@api_endpoint(spec_name="v3_complex", path="/users", methods="POST")
173+
def create_user_complex(body=None):
174+
return "Complex Success"
175+
176+
with patch("domaintools.request_validator.RequestValidator.validate") as mock_validate:
177+
create_user_complex(mock_client, body={"limit": 10})
178+
179+
# Verify the correct spec dictionary was retrieved and passed
180+
call_kwargs = mock_validate.call_args[1]
181+
assert call_kwargs["spec"]["info"]["title"] == "Complex Lookup Spec"
182+
assert call_kwargs["path"] == "/users"
183+
184+
def test_missing_spec_skips_validation(self, mock_client):
185+
"""
186+
If we ask for a spec name that isn't in the fixture, it should handle gracefully.
187+
"""
188+
189+
@api_endpoint(spec_name="non_existent_version", path="/users", methods="GET")
190+
def get_users():
191+
return "Ran Safe"
192+
193+
with patch("domaintools.request_validator.RequestValidator.validate") as mock_validate:
194+
result = get_users(mock_client)
195+
196+
assert result == "Ran Safe"
197+
mock_validate.assert_not_called()

0 commit comments

Comments
 (0)