Skip to content

Commit 3d68362

Browse files
committed
fix: correct import path for manual retry state and update route definition
- Fixed the import path for the manual retry state controller in routes.py. - Updated the route definition for manual retry to include a leading slash for consistency. - Added a new controller file for manual retry state functionality, implementing the logic for handling manual retries. - Introduced unit tests for the manual retry request and response models, ensuring validation and functionality. - Enhanced unit tests for the manual retry state route, covering various scenarios including valid and invalid API keys. These changes improve the structure and reliability of the manual retry feature.
1 parent 844f13a commit 3d68362

4 files changed

Lines changed: 329 additions & 4 deletions

File tree

File renamed without changes.

state-manager/app/routes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
# manual_retry
5454
from .models.manual_retry import ManualRetryRequestModel, ManualRetryResponseModel
55-
from .controller.manul_retry_state import manual_retry_state
55+
from .controller.manual_retry_state import manual_retry_state
5656

5757

5858
logger = LogsManager().get_logger()
@@ -181,7 +181,7 @@ async def re_enqueue_after_state_route(namespace_name: str, state_id: str, body:
181181
return await re_queue_after_signal(namespace_name, PydanticObjectId(state_id), body, x_exosphere_request_id)
182182

183183
@router.post(
184-
"state/{state_id}/manual-retry",
184+
"/state/{state_id}/manual-retry",
185185
response_model=ManualRetryResponseModel,
186186
status_code=status.HTTP_200_OK,
187187
response_description="State manual retry successfully",
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from app.models.manual_retry import ManualRetryRequestModel, ManualRetryResponseModel
5+
from app.models.state_status_enum import StateStatusEnum
6+
7+
8+
class TestManualRetryRequestModel:
9+
"""Test cases for ManualRetryRequestModel"""
10+
11+
def test_manual_retry_request_model_valid_data(self):
12+
"""Test ManualRetryRequestModel with valid fanout_id"""
13+
# Arrange & Act
14+
fanout_id = "test-fanout-id-123"
15+
model = ManualRetryRequestModel(fanout_id=fanout_id)
16+
17+
# Assert
18+
assert model.fanout_id == fanout_id
19+
20+
def test_manual_retry_request_model_empty_fanout_id(self):
21+
"""Test ManualRetryRequestModel with empty fanout_id"""
22+
# Arrange & Act
23+
fanout_id = ""
24+
model = ManualRetryRequestModel(fanout_id=fanout_id)
25+
26+
# Assert
27+
assert model.fanout_id == fanout_id
28+
29+
def test_manual_retry_request_model_uuid_fanout_id(self):
30+
"""Test ManualRetryRequestModel with UUID fanout_id"""
31+
# Arrange & Act
32+
fanout_id = "550e8400-e29b-41d4-a716-446655440000"
33+
model = ManualRetryRequestModel(fanout_id=fanout_id)
34+
35+
# Assert
36+
assert model.fanout_id == fanout_id
37+
38+
def test_manual_retry_request_model_long_fanout_id(self):
39+
"""Test ManualRetryRequestModel with long fanout_id"""
40+
# Arrange & Act
41+
fanout_id = "a" * 1000 # Very long string
42+
model = ManualRetryRequestModel(fanout_id=fanout_id)
43+
44+
# Assert
45+
assert model.fanout_id == fanout_id
46+
47+
def test_manual_retry_request_model_special_characters_fanout_id(self):
48+
"""Test ManualRetryRequestModel with special characters in fanout_id"""
49+
# Arrange & Act
50+
fanout_id = "test-fanout@#$%^&*()_+-={}[]|\\:;\"'<>?,./"
51+
model = ManualRetryRequestModel(fanout_id=fanout_id)
52+
53+
# Assert
54+
assert model.fanout_id == fanout_id
55+
56+
def test_manual_retry_request_model_missing_fanout_id(self):
57+
"""Test ManualRetryRequestModel with missing fanout_id field"""
58+
# Arrange & Act & Assert
59+
with pytest.raises(ValidationError) as exc_info:
60+
ManualRetryRequestModel() # type: ignore
61+
62+
assert "fanout_id" in str(exc_info.value)
63+
assert "Field required" in str(exc_info.value)
64+
65+
def test_manual_retry_request_model_none_fanout_id(self):
66+
"""Test ManualRetryRequestModel with None fanout_id"""
67+
# Arrange & Act & Assert
68+
with pytest.raises(ValidationError) as exc_info:
69+
ManualRetryRequestModel(fanout_id=None) # type: ignore
70+
71+
assert "fanout_id" in str(exc_info.value)
72+
73+
def test_manual_retry_request_model_numeric_fanout_id(self):
74+
"""Test ManualRetryRequestModel with numeric fanout_id (should fail validation)"""
75+
# Arrange & Act & Assert
76+
with pytest.raises(ValidationError) as exc_info:
77+
ManualRetryRequestModel(fanout_id=12345) # type: ignore
78+
79+
assert "string_type" in str(exc_info.value)
80+
81+
def test_manual_retry_request_model_dict_representation(self):
82+
"""Test ManualRetryRequestModel dict representation"""
83+
# Arrange & Act
84+
fanout_id = "test-fanout-id"
85+
model = ManualRetryRequestModel(fanout_id=fanout_id)
86+
87+
# Assert
88+
expected_dict = {"fanout_id": fanout_id}
89+
assert model.model_dump() == expected_dict
90+
91+
def test_manual_retry_request_model_json_serialization(self):
92+
"""Test ManualRetryRequestModel JSON serialization"""
93+
# Arrange & Act
94+
fanout_id = "test-fanout-id"
95+
model = ManualRetryRequestModel(fanout_id=fanout_id)
96+
97+
# Assert
98+
json_str = model.model_dump_json()
99+
assert f'"fanout_id":"{fanout_id}"' in json_str
100+
101+
102+
class TestManualRetryResponseModel:
103+
"""Test cases for ManualRetryResponseModel"""
104+
105+
def test_manual_retry_response_model_valid_data(self):
106+
"""Test ManualRetryResponseModel with valid data"""
107+
# Arrange & Act
108+
state_id = "507f1f77bcf86cd799439011"
109+
status = StateStatusEnum.CREATED
110+
model = ManualRetryResponseModel(id=state_id, status=status)
111+
112+
# Assert
113+
assert model.id == state_id
114+
assert model.status == status
115+
116+
def test_manual_retry_response_model_all_status_types(self):
117+
"""Test ManualRetryResponseModel with all possible status values"""
118+
# Arrange & Act & Assert
119+
state_id = "507f1f77bcf86cd799439011"
120+
121+
for status in StateStatusEnum:
122+
model = ManualRetryResponseModel(id=state_id, status=status)
123+
assert model.id == state_id
124+
assert model.status == status
125+
126+
def test_manual_retry_response_model_created_status(self):
127+
"""Test ManualRetryResponseModel with CREATED status"""
128+
# Arrange & Act
129+
state_id = "507f1f77bcf86cd799439011"
130+
status = StateStatusEnum.CREATED
131+
model = ManualRetryResponseModel(id=state_id, status=status)
132+
133+
# Assert
134+
assert model.id == state_id
135+
assert model.status == StateStatusEnum.CREATED
136+
137+
def test_manual_retry_response_model_retry_created_status(self):
138+
"""Test ManualRetryResponseModel with RETRY_CREATED status"""
139+
# Arrange & Act
140+
state_id = "507f1f77bcf86cd799439011"
141+
status = StateStatusEnum.RETRY_CREATED
142+
model = ManualRetryResponseModel(id=state_id, status=status)
143+
144+
# Assert
145+
assert model.id == state_id
146+
assert model.status == StateStatusEnum.RETRY_CREATED
147+
148+
def test_manual_retry_response_model_missing_id(self):
149+
"""Test ManualRetryResponseModel with missing id field"""
150+
# Arrange & Act & Assert
151+
with pytest.raises(ValidationError) as exc_info:
152+
ManualRetryResponseModel(status=StateStatusEnum.CREATED) # type: ignore
153+
154+
assert "id" in str(exc_info.value)
155+
assert "Field required" in str(exc_info.value)
156+
157+
def test_manual_retry_response_model_missing_status(self):
158+
"""Test ManualRetryResponseModel with missing status field"""
159+
# Arrange & Act & Assert
160+
with pytest.raises(ValidationError) as exc_info:
161+
ManualRetryResponseModel(id="507f1f77bcf86cd799439011") # type: ignore
162+
163+
assert "status" in str(exc_info.value)
164+
assert "Field required" in str(exc_info.value)
165+
166+
def test_manual_retry_response_model_none_id(self):
167+
"""Test ManualRetryResponseModel with None id"""
168+
# Arrange & Act & Assert
169+
with pytest.raises(ValidationError) as exc_info:
170+
ManualRetryResponseModel(id=None, status=StateStatusEnum.CREATED) # type: ignore
171+
172+
assert "id" in str(exc_info.value)
173+
174+
def test_manual_retry_response_model_none_status(self):
175+
"""Test ManualRetryResponseModel with None status"""
176+
# Arrange & Act & Assert
177+
with pytest.raises(ValidationError) as exc_info:
178+
ManualRetryResponseModel(id="507f1f77bcf86cd799439011", status=None) # type: ignore
179+
180+
assert "status" in str(exc_info.value)
181+
182+
def test_manual_retry_response_model_invalid_status(self):
183+
"""Test ManualRetryResponseModel with invalid status"""
184+
# Arrange & Act & Assert
185+
with pytest.raises(ValidationError) as exc_info:
186+
ManualRetryResponseModel(id="507f1f77bcf86cd799439011", status="INVALID_STATUS") # type: ignore
187+
188+
assert "status" in str(exc_info.value)
189+
190+
def test_manual_retry_response_model_numeric_id(self):
191+
"""Test ManualRetryResponseModel with numeric id (should fail validation)"""
192+
# Arrange & Act & Assert
193+
with pytest.raises(ValidationError) as exc_info:
194+
ManualRetryResponseModel(id=12345, status=StateStatusEnum.CREATED) # type: ignore
195+
196+
assert "string_type" in str(exc_info.value)
197+
198+
def test_manual_retry_response_model_dict_representation(self):
199+
"""Test ManualRetryResponseModel dict representation"""
200+
# Arrange & Act
201+
state_id = "507f1f77bcf86cd799439011"
202+
status = StateStatusEnum.CREATED
203+
model = ManualRetryResponseModel(id=state_id, status=status)
204+
205+
# Assert
206+
expected_dict = {"id": state_id, "status": status}
207+
assert model.model_dump() == expected_dict
208+
209+
def test_manual_retry_response_model_json_serialization(self):
210+
"""Test ManualRetryResponseModel JSON serialization"""
211+
# Arrange & Act
212+
state_id = "507f1f77bcf86cd799439011"
213+
status = StateStatusEnum.CREATED
214+
model = ManualRetryResponseModel(id=state_id, status=status)
215+
216+
# Assert
217+
json_str = model.model_dump_json()
218+
assert f'"id":"{state_id}"' in json_str
219+
assert f'"status":"{status.value}"' in json_str
220+
221+
def test_manual_retry_response_model_empty_id(self):
222+
"""Test ManualRetryResponseModel with empty string id"""
223+
# Arrange & Act
224+
state_id = ""
225+
status = StateStatusEnum.CREATED
226+
model = ManualRetryResponseModel(id=state_id, status=status)
227+
228+
# Assert
229+
assert model.id == state_id
230+
assert model.status == status
231+
232+
def test_manual_retry_response_model_long_id(self):
233+
"""Test ManualRetryResponseModel with very long id"""
234+
# Arrange & Act
235+
state_id = "a" * 1000 # Very long string
236+
status = StateStatusEnum.CREATED
237+
model = ManualRetryResponseModel(id=state_id, status=status)
238+
239+
# Assert
240+
assert model.id == state_id
241+
assert model.status == status

state-manager/tests/unit/test_routes.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from app.models.secrets_response import SecretsResponseModel
99
from app.models.list_models import ListRegisteredNodesResponse, ListGraphTemplatesResponse
1010
from app.models.run_models import RunsResponse, RunListItem, RunStatusEnum
11+
from app.models.manual_retry import ManualRetryRequestModel, ManualRetryResponseModel
1112

1213

1314
import pytest
@@ -32,6 +33,7 @@ def test_router_has_correct_routes(self):
3233
assert any('/v0/namespace/{namespace_name}/state/{state_id}/errored' in path for path in paths)
3334
assert any('/v0/namespace/{namespace_name}/state/{state_id}/prune' in path for path in paths)
3435
assert any('/v0/namespace/{namespace_name}/state/{state_id}/re-enqueue-after' in path for path in paths)
36+
assert any('/v0/namespace/{namespace_name}/state/{state_id}/manual-retry' in path for path in paths)
3537

3638
# Graph template routes (there are two /graph/{graph_name} routes - GET and PUT)
3739
assert any('/v0/namespace/{namespace_name}/graph/{graph_name}' in path for path in paths)
@@ -273,6 +275,26 @@ def test_list_graph_templates_response_validation(self):
273275
assert model.namespace == "test"
274276
assert model.count == 0
275277

278+
def test_manual_retry_request_model_validation(self):
279+
"""Test ManualRetryRequestModel validation"""
280+
# Test with valid data
281+
valid_data = {"fanout_id": "test-fanout-id-123"}
282+
model = ManualRetryRequestModel(**valid_data)
283+
assert model.fanout_id == "test-fanout-id-123"
284+
285+
def test_manual_retry_response_model_validation(self):
286+
"""Test ManualRetryResponseModel validation"""
287+
from app.models.state_status_enum import StateStatusEnum
288+
289+
# Test with valid data
290+
valid_data = {
291+
"id": "507f1f77bcf86cd799439011",
292+
"status": StateStatusEnum.CREATED
293+
}
294+
model = ManualRetryResponseModel(**valid_data)
295+
assert model.id == "507f1f77bcf86cd799439011"
296+
assert model.status == StateStatusEnum.CREATED
297+
276298

277299

278300

@@ -295,7 +317,8 @@ def test_route_handlers_exist(self):
295317
list_graph_templates_route,
296318
get_runs_route,
297319
get_graph_structure_route,
298-
get_node_run_details_route
320+
get_node_run_details_route,
321+
manual_retry_state_route
299322

300323
)
301324

@@ -313,6 +336,7 @@ def test_route_handlers_exist(self):
313336
assert callable(get_runs_route)
314337
assert callable(get_graph_structure_route)
315338
assert callable(get_node_run_details_route)
339+
assert callable(manual_retry_state_route)
316340

317341

318342

@@ -1033,4 +1057,64 @@ async def test_get_node_run_details_route_with_invalid_api_key(self, mock_get_no
10331057

10341058
assert exc_info.value.status_code == 401
10351059
assert exc_info.value.detail == "Invalid API key"
1036-
mock_get_node_run_details.assert_not_called()
1060+
mock_get_node_run_details.assert_not_called()
1061+
1062+
@patch('app.routes.manual_retry_state')
1063+
async def test_manual_retry_state_route_with_valid_api_key(self, mock_manual_retry_state, mock_request):
1064+
"""Test manual_retry_state_route with valid API key"""
1065+
from app.routes import manual_retry_state_route
1066+
1067+
# Arrange
1068+
mock_manual_retry_state.return_value = MagicMock()
1069+
body = ManualRetryRequestModel(fanout_id="test-fanout-id")
1070+
1071+
# Act
1072+
result = await manual_retry_state_route("test_namespace", "507f1f77bcf86cd799439011", body, mock_request, "valid_key")
1073+
1074+
# Assert
1075+
mock_manual_retry_state.assert_called_once()
1076+
call_args = mock_manual_retry_state.call_args
1077+
assert call_args[0][0] == "test_namespace" # namespace_name
1078+
assert str(call_args[0][1]) == "507f1f77bcf86cd799439011" # state_id as PydanticObjectId
1079+
assert call_args[0][2] == body # body
1080+
assert call_args[0][3] == "test-request-id" # x_exosphere_request_id
1081+
assert result == mock_manual_retry_state.return_value
1082+
1083+
@patch('app.routes.manual_retry_state')
1084+
async def test_manual_retry_state_route_with_invalid_api_key(self, mock_manual_retry_state, mock_request):
1085+
"""Test manual_retry_state_route with invalid API key"""
1086+
from app.routes import manual_retry_state_route
1087+
from fastapi import HTTPException
1088+
1089+
# Arrange
1090+
body = ManualRetryRequestModel(fanout_id="test-fanout-id")
1091+
1092+
# Act & Assert
1093+
with pytest.raises(HTTPException) as exc_info:
1094+
await manual_retry_state_route("test_namespace", "507f1f77bcf86cd799439011", body, mock_request, None) # type: ignore
1095+
1096+
assert exc_info.value.status_code == 401
1097+
assert exc_info.value.detail == "Invalid API key"
1098+
mock_manual_retry_state.assert_not_called()
1099+
1100+
@patch('app.routes.manual_retry_state')
1101+
async def test_manual_retry_state_route_without_request_id(self, mock_manual_retry_state, mock_request_no_id):
1102+
"""Test manual_retry_state_route without x_exosphere_request_id"""
1103+
from app.routes import manual_retry_state_route
1104+
1105+
# Arrange
1106+
mock_manual_retry_state.return_value = MagicMock()
1107+
body = ManualRetryRequestModel(fanout_id="test-fanout-id")
1108+
1109+
# Act
1110+
result = await manual_retry_state_route("test_namespace", "507f1f77bcf86cd799439011", body, mock_request_no_id, "valid_key")
1111+
1112+
# Assert
1113+
mock_manual_retry_state.assert_called_once()
1114+
call_args = mock_manual_retry_state.call_args
1115+
assert call_args[0][0] == "test_namespace" # namespace_name
1116+
assert str(call_args[0][1]) == "507f1f77bcf86cd799439011" # state_id as PydanticObjectId
1117+
assert call_args[0][2] == body # body
1118+
# Should generate a UUID when no request ID is present
1119+
assert len(call_args[0][3]) > 0 # x_exosphere_request_id should be generated
1120+
assert result == mock_manual_retry_state.return_value

0 commit comments

Comments
 (0)