Skip to content

Commit ccc0205

Browse files
committed
test: add client template format tests and per-request scheduler header tests
- test_client_template_format.py: 12 onmoto tests verifying the client template format works correctly — launchTemplateId without imageId, no providerApi defaulting to EC2Fleet, field mapping, EC2Fleet spot provisioning with external launch template - test_scheduler_header.py: 10 integration tests documenting the per-request X-ORB-Scheduler header feature — dependency-level tests verifying get_request_formatter and get_request_scheduler behaviour, plus a static analysis test that will fail when routes are wired
1 parent c0d736a commit ccc0205

File tree

2 files changed

+752
-0
lines changed

2 files changed

+752
-0
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
"""Integration tests for X-ORB-Scheduler per-request header feature.
2+
3+
Documents the current wiring state of get_request_formatter / get_request_scheduler
4+
in dependencies.py and verifies the feature behaviour end-to-end.
5+
6+
Key findings (read before editing):
7+
- get_response_formatting_service() is what the routers actually use (FORMATTER dep).
8+
- get_request_formatter() exists in dependencies.py and IS header-aware, but is NOT
9+
wired into any router — the routers all use get_response_formatting_service instead.
10+
- Therefore the X-ORB-Scheduler header has NO effect on live routes today.
11+
- These tests document that gap and verify the dependency function itself works correctly
12+
in isolation, so a future wiring change can be validated by updating the "not wired"
13+
tests to assert the header IS honoured.
14+
"""
15+
16+
import sys
17+
from pathlib import Path
18+
from unittest.mock import MagicMock
19+
20+
import pytest
21+
22+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
23+
24+
25+
# ---------------------------------------------------------------------------
26+
# Helpers
27+
# ---------------------------------------------------------------------------
28+
29+
30+
def _make_mock_scheduler(name: str = "mock") -> MagicMock:
31+
scheduler = MagicMock()
32+
scheduler.name = name
33+
scheduler.format_request_response.side_effect = lambda raw: {**raw, "scheduler": name}
34+
scheduler.format_request_status_response.side_effect = lambda reqs: {
35+
"requests": reqs,
36+
"scheduler": name,
37+
}
38+
scheduler.format_machine_status_response.side_effect = lambda machines: {
39+
"machines": machines,
40+
"scheduler": name,
41+
}
42+
scheduler.get_exit_code_for_status.return_value = 0
43+
return scheduler
44+
45+
46+
def _make_formatting_service(scheduler_name: str = "default"):
47+
from orb.interface.response_formatting_service import ResponseFormattingService
48+
49+
return ResponseFormattingService(_make_mock_scheduler(scheduler_name))
50+
51+
52+
# ---------------------------------------------------------------------------
53+
# Unit tests: get_request_formatter dependency function
54+
# ---------------------------------------------------------------------------
55+
56+
57+
class TestGetRequestFormatterDependency:
58+
"""Tests for the get_request_formatter dependency in isolation."""
59+
60+
def _make_request(self, headers: dict) -> MagicMock:
61+
req = MagicMock()
62+
req.headers = headers
63+
return req
64+
65+
def _make_container(self, scheduler_name: str = "default") -> MagicMock:
66+
container = MagicMock()
67+
container.get.return_value = _make_formatting_service(scheduler_name)
68+
return container
69+
70+
def test_no_header_returns_default_service(self):
71+
"""Without X-ORB-Scheduler header, returns the container's ResponseFormattingService."""
72+
from orb.api.dependencies import get_request_formatter
73+
74+
request = self._make_request({})
75+
container = self._make_container("default")
76+
77+
result = get_request_formatter(request=request, container=container)
78+
79+
from orb.interface.response_formatting_service import ResponseFormattingService
80+
81+
assert isinstance(result, ResponseFormattingService)
82+
container.get.assert_called_once_with(ResponseFormattingService)
83+
84+
def test_unknown_scheduler_header_falls_back_to_default(self):
85+
"""X-ORB-Scheduler with an unregistered value falls back to container default."""
86+
from orb.api.dependencies import get_request_formatter
87+
88+
request = self._make_request({"X-ORB-Scheduler": "nonexistent-scheduler-xyz"})
89+
container = self._make_container("default")
90+
91+
result = get_request_formatter(request=request, container=container)
92+
93+
from orb.interface.response_formatting_service import ResponseFormattingService
94+
95+
assert isinstance(result, ResponseFormattingService)
96+
97+
def test_registered_scheduler_header_returns_new_service(self):
98+
"""X-ORB-Scheduler with a registered type returns a freshly-built ResponseFormattingService."""
99+
from orb.api.dependencies import get_request_formatter
100+
from orb.infrastructure.scheduler.hostfactory.hostfactory_strategy import (
101+
HostFactorySchedulerStrategy,
102+
)
103+
from orb.infrastructure.scheduler.registry import get_scheduler_registry
104+
from orb.interface.response_formatting_service import ResponseFormattingService
105+
106+
registry = get_scheduler_registry()
107+
if not registry.is_registered("hostfactory"):
108+
registry.register(
109+
"hostfactory",
110+
lambda cfg: HostFactorySchedulerStrategy(),
111+
lambda c: None,
112+
strategy_class=HostFactorySchedulerStrategy,
113+
)
114+
115+
request = self._make_request({"X-ORB-Scheduler": "hostfactory"})
116+
container = self._make_container("default")
117+
118+
result = get_request_formatter(request=request, container=container)
119+
120+
assert isinstance(result, ResponseFormattingService)
121+
122+
def test_get_request_scheduler_no_header_returns_default(self):
123+
"""get_request_scheduler without header returns container's SchedulerPort."""
124+
from orb.api.dependencies import get_request_scheduler
125+
from orb.application.ports.scheduler_port import SchedulerPort
126+
127+
mock_scheduler = _make_mock_scheduler("default")
128+
container = MagicMock()
129+
container.get.return_value = mock_scheduler
130+
131+
request = self._make_request({})
132+
result = get_request_scheduler(request=request, container=container)
133+
134+
assert result is mock_scheduler
135+
container.get.assert_called_once_with(SchedulerPort)
136+
137+
def test_get_request_scheduler_unknown_header_falls_back(self):
138+
"""get_request_scheduler with unknown header falls back to container default."""
139+
from orb.api.dependencies import get_request_scheduler
140+
141+
mock_scheduler = _make_mock_scheduler("default")
142+
container = MagicMock()
143+
container.get.return_value = mock_scheduler
144+
145+
request = self._make_request({"X-ORB-Scheduler": "does-not-exist"})
146+
result = get_request_scheduler(request=request, container=container)
147+
148+
assert result is mock_scheduler
149+
150+
151+
# ---------------------------------------------------------------------------
152+
# Integration tests: header NOT wired into live routes (documents current gap)
153+
# ---------------------------------------------------------------------------
154+
155+
156+
class TestSchedulerHeaderNotWiredInRoutes:
157+
"""Documents that X-ORB-Scheduler header is NOT currently wired into the routers.
158+
159+
The routers use FORMATTER = Depends(get_response_formatting_service), which
160+
ignores the header entirely. These tests confirm the current behaviour so that
161+
when the feature is wired, the tests will need updating.
162+
"""
163+
164+
@pytest.fixture
165+
def app_with_real_routes(self):
166+
"""FastAPI app with real routers and overridden dependencies."""
167+
import orb.api.dependencies as deps
168+
from orb.api.server import create_fastapi_app
169+
from orb.config.schemas.server_schema import AuthConfig, ServerConfig
170+
171+
server_config = ServerConfig( # type: ignore[call-arg]
172+
enabled=True,
173+
auth=AuthConfig(enabled=False, strategy="replace"), # type: ignore[call-arg]
174+
)
175+
app = create_fastapi_app(server_config)
176+
177+
# Wire up minimal mocks so routes don't hit real DI
178+
default_svc = _make_formatting_service("default")
179+
app.dependency_overrides[deps.get_response_formatting_service] = lambda: default_svc
180+
181+
mock_health = MagicMock()
182+
mock_health.get_status.return_value = {"status": "healthy"}
183+
app.dependency_overrides[deps.get_health_check_port] = lambda: mock_health
184+
185+
return app
186+
187+
def test_x_orb_scheduler_header_present_in_request_reaches_server(
188+
self, app_with_real_routes
189+
):
190+
"""A request with X-ORB-Scheduler header is accepted (not rejected by middleware)."""
191+
from fastapi.testclient import TestClient
192+
193+
client = TestClient(app_with_real_routes, raise_server_exceptions=False)
194+
response = client.get(
195+
"/health",
196+
headers={"X-ORB-Scheduler": "hostfactory"},
197+
)
198+
# Header must not cause a 4xx/5xx at the middleware level
199+
assert response.status_code < 500
200+
201+
def test_routes_use_get_response_formatting_service_not_get_request_formatter(self):
202+
"""Confirm routers import get_response_formatting_service, not get_request_formatter.
203+
204+
This is a static analysis test — it reads the router source and checks which
205+
dependency function is referenced. Fails if the wiring changes (intentionally).
206+
"""
207+
import ast
208+
209+
routers_dir = (
210+
Path(__file__).parent.parent.parent
211+
/ "src" / "orb" / "api" / "routers"
212+
)
213+
for router_file in routers_dir.glob("*.py"):
214+
source = router_file.read_text()
215+
tree = ast.parse(source)
216+
names_used = {
217+
node.id
218+
for node in ast.walk(tree)
219+
if isinstance(node, ast.Name)
220+
}
221+
# Current state: routers use get_response_formatting_service
222+
if "get_response_formatting_service" in source:
223+
assert "get_response_formatting_service" in names_used, (
224+
f"{router_file.name} imports but does not use get_response_formatting_service"
225+
)
226+
# Current state: no router uses get_request_formatter
227+
assert "get_request_formatter" not in names_used, (
228+
f"{router_file.name} now uses get_request_formatter — "
229+
"update TestSchedulerHeaderWiredInRoutes to verify header behaviour"
230+
)
231+
232+
233+
# ---------------------------------------------------------------------------
234+
# Unit tests: ResponseFormattingService produces scheduler-specific output
235+
# ---------------------------------------------------------------------------
236+
237+
238+
class TestResponseFormattingServiceSchedulerOutput:
239+
"""Verify that swapping the scheduler in ResponseFormattingService changes output.
240+
241+
This is the core contract that the header feature relies on: different scheduler
242+
strategies produce different response shapes.
243+
"""
244+
245+
def test_format_request_operation_uses_injected_scheduler(self):
246+
"""format_request_operation delegates to the injected scheduler."""
247+
from orb.interface.response_formatting_service import ResponseFormattingService
248+
249+
scheduler = _make_mock_scheduler("hf")
250+
svc = ResponseFormattingService(scheduler)
251+
252+
result = svc.format_request_operation({"request_id": "req-1", "status": "pending"}, "pending")
253+
254+
scheduler.format_request_response.assert_called_once()
255+
assert result.data.get("scheduler") == "hf"
256+
257+
def test_format_request_status_uses_injected_scheduler(self):
258+
"""format_request_status delegates to the injected scheduler."""
259+
from orb.interface.response_formatting_service import ResponseFormattingService
260+
261+
scheduler = _make_mock_scheduler("default")
262+
svc = ResponseFormattingService(scheduler)
263+
264+
result = svc.format_request_status([{"request_id": "req-1"}])
265+
266+
scheduler.format_request_status_response.assert_called_once()
267+
assert result.data.get("scheduler") == "default"
268+
269+
def test_two_services_with_different_schedulers_produce_different_output(self):
270+
"""Two ResponseFormattingService instances with different schedulers differ in output."""
271+
from orb.interface.response_formatting_service import ResponseFormattingService
272+
273+
svc_hf = ResponseFormattingService(_make_mock_scheduler("hostfactory"))
274+
svc_default = ResponseFormattingService(_make_mock_scheduler("default"))
275+
276+
raw = {"request_id": "req-x", "status": "pending"}
277+
out_hf = svc_hf.format_request_operation(raw, "pending")
278+
out_default = svc_default.format_request_operation(raw, "pending")
279+
280+
assert out_hf.data.get("scheduler") == "hostfactory"
281+
assert out_default.data.get("scheduler") == "default"
282+
assert out_hf.data != out_default.data

0 commit comments

Comments
 (0)