Skip to content

Commit 55c76cb

Browse files
committed
IDEV-2204: Add tests.
1 parent 0d7033f commit 55c76cb

File tree

1 file changed

+293
-0
lines changed

1 file changed

+293
-0
lines changed

tests/test_docstring_patcher.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import pytest
2+
import inspect
3+
import functools
4+
5+
from domaintools.docstring_patcher import DocstringPatcher
6+
7+
8+
@pytest.fixture
9+
def patcher():
10+
"""Returns an instance of the class under test."""
11+
return DocstringPatcher()
12+
13+
14+
@pytest.fixture(scope="module")
15+
def sample_spec():
16+
"""
17+
Provides a comprehensive, reusable mock OpenAPI spec dictionary.
18+
"""
19+
return {
20+
"openapi": "3.0.0",
21+
"info": {"title": "Test API", "version": "1.0.0"},
22+
"components": {
23+
"parameters": {
24+
"LimitParam": {
25+
"name": "limit",
26+
"in": "query",
27+
"description": "Max number of items to return.",
28+
"schema": {"type": "integer"},
29+
}
30+
},
31+
"schemas": {
32+
"User": {
33+
"type": "object",
34+
"properties": {"name": {"type": "string"}},
35+
}
36+
},
37+
"requestBodies": {
38+
"UserBody": {
39+
"description": "User object to create.",
40+
"required": True,
41+
"content": {
42+
"application/json": {"schema": {"$ref": "#/components/schemas/User"}}
43+
},
44+
}
45+
},
46+
},
47+
"paths": {
48+
"/users": {
49+
"get": {
50+
"summary": "Get all users",
51+
"description": "Returns a list of users.",
52+
"externalDocs": {"url": "http://docs.example.com/get-users"},
53+
"parameters": [
54+
{
55+
"name": "status",
56+
"in": "query",
57+
"required": True,
58+
"description": "User's current status.",
59+
"schema": {"type": "string"},
60+
},
61+
{"$ref": "#/components/parameters/LimitParam"},
62+
],
63+
},
64+
"post": {
65+
"summary": "Create a new user",
66+
"description": "Creates a single new user.",
67+
"requestBody": {"$ref": "#/components/requestBodies/UserBody"},
68+
},
69+
},
70+
"/pets/{petId}": {
71+
"get": {
72+
"summary": "Get a single pet",
73+
"description": "Returns one pet by ID.",
74+
}
75+
},
76+
"/health": {
77+
# This path exists, but has no operations (get, post, etc.)
78+
"description": "Health check path."
79+
},
80+
},
81+
}
82+
83+
84+
@pytest.fixture
85+
def mock_api_instance(sample_spec):
86+
"""
87+
Provides a mock API instance with decorated methods
88+
that the DocstringPatcher will look for.
89+
"""
90+
91+
# This decorator mimics the one you'd use in your real API class
92+
def api_endpoint(spec_name, path):
93+
def decorator(func):
94+
func._api_spec_name = spec_name
95+
func._api_path = path
96+
97+
@functools.wraps(func)
98+
def wrapper(self, *args, **kwargs):
99+
return func(*args, **kwargs)
100+
101+
return wrapper
102+
103+
return decorator
104+
105+
class MockAPI:
106+
def __init__(self, specs):
107+
# The patcher expects the instance to have a 'specs' attribute
108+
self.specs = specs
109+
110+
@api_endpoint(spec_name="v1", path="/users")
111+
def user_operations(self):
112+
"""This is the original user docstring."""
113+
return "user_operations_called"
114+
115+
@api_endpoint(spec_name="v1", path="/pets/{petId}")
116+
def get_pet(self):
117+
"""Original pet docstring."""
118+
return "get_pet_called"
119+
120+
@api_endpoint(spec_name="v1", path="/health")
121+
def health_check(self):
122+
"""Original health docstring."""
123+
return "health_check_called"
124+
125+
@api_endpoint(spec_name="v2_nonexistent", path="/users")
126+
def bad_spec_name(self):
127+
"""Original bad spec docstring."""
128+
return "bad_spec_called"
129+
130+
@api_endpoint(spec_name="v1", path="/nonexistent-path")
131+
def bad_path(self):
132+
"""Original bad path docstring."""
133+
return "bad_path_called"
134+
135+
def not_an_api_method(self):
136+
"""Internal method docstring."""
137+
return "internal_called"
138+
139+
# Create an instance, passing in the spec fixture
140+
api_instance = MockAPI(specs={"v1": sample_spec})
141+
return api_instance
142+
143+
144+
# --- Test Cases ---
145+
146+
147+
def test_patch_method_still_callable(patcher, mock_api_instance):
148+
"""
149+
Ensures that after patching, the method can still be called
150+
and returns its original value.
151+
"""
152+
# Act
153+
patcher.patch(mock_api_instance)
154+
155+
# Assert
156+
assert mock_api_instance.user_operations() == "user_operations_called"
157+
assert mock_api_instance.get_pet() == "get_pet_called"
158+
159+
160+
def test_patch_leaves_unmarked_methods_alone(patcher, mock_api_instance):
161+
"""
162+
Tests that methods without the decorator are not modified.
163+
"""
164+
# Arrange
165+
original_doc = inspect.getdoc(mock_api_instance.not_an_api_method)
166+
167+
# Act
168+
patcher.patch(mock_api_instance)
169+
170+
# Assert
171+
new_doc = inspect.getdoc(mock_api_instance.not_an_api_method)
172+
assert new_doc == original_doc
173+
assert new_doc == "Internal method docstring."
174+
175+
176+
def test_patch_preserves_original_docstring(patcher, mock_api_instance):
177+
"""
178+
Tests that the new docstring starts with the original docstring.
179+
"""
180+
# Arrange
181+
original_doc = inspect.getdoc(mock_api_instance.user_operations)
182+
assert original_doc == "This is the original user docstring."
183+
184+
# Act
185+
patcher.patch(mock_api_instance)
186+
187+
# Assert
188+
new_doc = inspect.getdoc(mock_api_instance.user_operations)
189+
assert new_doc.startswith(original_doc)
190+
assert len(new_doc) > len(original_doc)
191+
192+
193+
def test_patch_handles_multiple_operations(patcher, mock_api_instance):
194+
"""
195+
Tests that a single method gets docs for ALL operations
196+
on its path (e.g., GET and POST for /users).
197+
"""
198+
# Act
199+
patcher.patch(mock_api_instance)
200+
new_doc = inspect.getdoc(mock_api_instance.user_operations)
201+
202+
# Assert
203+
# Check for original doc
204+
assert new_doc.startswith("This is the original user docstring.")
205+
206+
# Check for GET operation details
207+
assert "--- Operation: GET /users ---" in new_doc
208+
assert "Summary: Get all users" in new_doc
209+
assert "External Doc: http://docs.example.com/get-users" in new_doc
210+
assert "**status** (string)" in new_doc
211+
assert "Required: True" in new_doc
212+
assert "Description: User's current status." in new_doc
213+
214+
# Check for $ref'd query param
215+
assert "**limit** (integer)" in new_doc
216+
assert "Description: Max number of items to return." in new_doc
217+
218+
# Check for POST operation details
219+
assert "--- Operation: POST /users ---" in new_doc
220+
assert "Summary: Create a new user" in new_doc
221+
assert "Request Body:" in new_doc
222+
223+
# Check for $ref'd request body
224+
assert "**User**" in new_doc
225+
assert "Description: User object to create." in new_doc
226+
assert "Required: True" in new_doc
227+
228+
229+
def test_patch_handles_single_operation(patcher, mock_api_instance):
230+
"""
231+
Tests a path with only one operation (GET) and no params/body.
232+
"""
233+
# Act
234+
patcher.patch(mock_api_instance)
235+
new_doc = inspect.getdoc(mock_api_instance.get_pet)
236+
237+
# Assert
238+
assert new_doc.startswith("Original pet docstring.")
239+
assert "--- Operation: GET /pets/{petId} ---" in new_doc
240+
assert "Summary: Get a single pet" in new_doc
241+
242+
# Check for empty sections
243+
assert "Query Parameters:\n (No query parameters)" in new_doc
244+
assert "Request Body:\n (No request body)" in new_doc
245+
246+
# Ensure other methods aren't included
247+
assert "--- Operation: POST /pets/{petId} ---" not in new_doc
248+
249+
250+
def test_patch_spec_not_found(patcher, mock_api_instance):
251+
"""
252+
Tests that an error message is added to the doc if the
253+
spec name (e.g., 'v2_nonexistent') isn't in the instance's 'specs' dict.
254+
"""
255+
# Act
256+
patcher.patch(mock_api_instance)
257+
new_doc = inspect.getdoc(mock_api_instance.bad_spec_name)
258+
259+
# Assert
260+
assert new_doc.startswith("Original bad spec docstring.")
261+
# Check for the specific error message your code generates
262+
assert "--- API Details Error ---" in new_doc
263+
assert "(Could not find any operations for path '/users')" in new_doc
264+
265+
266+
def test_patch_path_not_found(patcher, mock_api_instance):
267+
"""
268+
Tests that an error message is added if the path exists on the method
269+
but not in the spec file.
270+
"""
271+
# Act
272+
patcher.patch(mock_api_instance)
273+
new_doc = inspect.getdoc(mock_api_instance.bad_path)
274+
275+
# Assert
276+
assert new_doc.startswith("Original bad path docstring.")
277+
assert "--- API Details Error ---" in new_doc
278+
assert "(Could not find any operations for path '/nonexistent-path')" in new_doc
279+
280+
281+
def test_patch_path_found_but_no_operations(patcher, mock_api_instance):
282+
"""
283+
Tests that an error message is added if the path (/health) is in
284+
the spec but has no operations (get, post, etc.) defined.
285+
"""
286+
# Act
287+
patcher.patch(mock_api_instance)
288+
new_doc = inspect.getdoc(mock_api_instance.health_check)
289+
290+
# Assert
291+
assert new_doc.startswith("Original health docstring.")
292+
assert "--- API Details Error ---" in new_doc
293+
assert "(Could not find any operations for path '/health')" in new_doc

0 commit comments

Comments
 (0)