Skip to content

Commit c333b46

Browse files
committed
test: shared conftest + import-guards + storage-deserialize health probe
1 parent 6525700 commit c333b46

6 files changed

Lines changed: 303 additions & 45 deletions

File tree

tests/conftest.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,18 @@ def _is_moto_test(item) -> bool:
4949

5050

5151
def pytest_collection_modifyitems(config, items):
52+
# Live tests require real AWS credentials and a real account: they must be
53+
# opted into explicitly with --live (or --run-aws). We deliberately do NOT
54+
# treat the presence of "live" / "onaws" in the CLI arg path as an implicit
55+
# opt-in: pointing pytest at tests/providers/aws/live to collect the suite
56+
# (e.g. the providers-serial CI job) would otherwise auto-enable live mode
57+
# without credentials and every test would error on the missing config
58+
# file. The path-based shortcut also makes it dangerously easy for an
59+
# operator to launch real AWS provisioning by accident.
5260
live_enabled = config.getoption("--live") or config.getoption("--run-aws")
5361
no_mocked = config.getoption("--no-mocked")
5462
provider_filter = config.getoption("--provider")
5563

56-
# Also honour legacy path-based live detection (e.g. pytest tests/providers/aws/live)
57-
explicit_live = any("live" in str(a) or "onaws" in str(a) for a in config.args)
58-
if explicit_live:
59-
live_enabled = True
60-
6164
skip_live = pytest.mark.skip(
6265
reason="requires real credentials — pass --live (or --run-aws) to run"
6366
)

tests/docker/conftest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
"""Mark all docker tests as slow — they require Docker daemon and real builds."""
1+
"""Docker tests: slow + serial.
2+
3+
slow: require a Docker daemon + real image builds.
4+
serial: bind a fixed host port (8003) and a fixed container name
5+
(orb-integration-test). Parallel workers would collide.
6+
"""
27

38
import pytest
49

510

611
def pytest_collection_modifyitems(items):
7-
"""Mark all tests in the docker directory as slow."""
12+
"""Mark all tests in the docker directory as slow + serial."""
813
for item in items:
914
if "tests/docker" in str(item.fspath):
1015
item.add_marker(pytest.mark.slow)
16+
item.add_marker(pytest.mark.serial)

tests/e2e/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""End-to-end test configuration.
2+
3+
E2E tests use unittest-class ``setUp`` with ``tempfile.mkdtemp()`` and
4+
several class-instance side effects that can race under xdist. Mark
5+
every test in this tree serial; xdist runners can still pick them up,
6+
but they will be scheduled sequentially via the ``serial`` marker.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import pytest
12+
13+
14+
def pytest_collection_modifyitems(config, items):
15+
marker = pytest.mark.serial
16+
for item in items:
17+
item.add_marker(marker)
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""Tests for the storage-aware ``database`` health check wiring."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import Any
7+
8+
from orb.monitoring.health import (
9+
HealthCheck,
10+
HealthCheckConfig,
11+
register_storage_health_checks,
12+
)
13+
14+
15+
def _build_health_check(tmp_path: Path) -> HealthCheck:
16+
return HealthCheck(config=HealthCheckConfig(health_dir=tmp_path))
17+
18+
19+
# ── register_storage_health_checks ─────────────────────────────────────────
20+
21+
22+
def test_register_replaces_default_database_check_with_healthy_storage(
23+
tmp_path: Path,
24+
) -> None:
25+
hc = _build_health_check(tmp_path)
26+
assert hc.run_check("database")["status"] == "unknown" # placeholder
27+
28+
class FakeStorage:
29+
def is_healthy(self) -> tuple[bool, dict[str, Any]]:
30+
return True, {"type": "json", "entity_count": 3}
31+
32+
register_storage_health_checks(hc, FakeStorage())
33+
result = hc.run_check("database")
34+
assert result["status"] == "healthy"
35+
assert result["details"] == {"type": "json", "entity_count": 3}
36+
37+
38+
def test_register_marks_unhealthy_when_storage_reports_unhealthy(
39+
tmp_path: Path,
40+
) -> None:
41+
hc = _build_health_check(tmp_path)
42+
43+
class BrokenStorage:
44+
def is_healthy(self) -> tuple[bool, dict[str, Any]]:
45+
return False, {"type": "sql", "reason": "connection refused"}
46+
47+
register_storage_health_checks(hc, BrokenStorage())
48+
result = hc.run_check("database")
49+
assert result["status"] == "unhealthy"
50+
assert result["details"]["reason"] == "connection refused"
51+
52+
53+
def test_register_handles_exception_from_probe(tmp_path: Path) -> None:
54+
hc = _build_health_check(tmp_path)
55+
56+
class CrashStorage:
57+
def is_healthy(self) -> tuple[bool, dict[str, Any]]:
58+
raise RuntimeError("boom")
59+
60+
register_storage_health_checks(hc, CrashStorage())
61+
result = hc.run_check("database")
62+
assert result["status"] == "unhealthy"
63+
assert "boom" in result["details"]["error"]
64+
65+
66+
def test_register_tolerates_bare_bool_return(tmp_path: Path) -> None:
67+
"""Legacy strategies might return a bare bool — wrap it cleanly."""
68+
hc = _build_health_check(tmp_path)
69+
70+
class LegacyStorage:
71+
def is_healthy(self) -> bool:
72+
return True
73+
74+
register_storage_health_checks(hc, LegacyStorage())
75+
result = hc.run_check("database")
76+
assert result["status"] == "healthy"
77+
78+
79+
def test_register_no_op_when_storage_lacks_is_healthy(tmp_path: Path) -> None:
80+
"""Storage objects without is_healthy keep the placeholder check."""
81+
hc = _build_health_check(tmp_path)
82+
83+
class NoHealthApi:
84+
pass
85+
86+
register_storage_health_checks(hc, NoHealthApi())
87+
result = hc.run_check("database")
88+
assert result["status"] == "unknown" # placeholder still in place
89+
90+
91+
def test_register_uses_force_to_override_existing(tmp_path: Path) -> None:
92+
"""The constructor pre-registers a placeholder; we must overwrite it."""
93+
hc = _build_health_check(tmp_path)
94+
# Sanity: default placeholder is in place.
95+
assert hc.checks["database"].__name__ == "_check_database_health"
96+
97+
class FakeStorage:
98+
def is_healthy(self) -> tuple[bool, dict[str, Any]]:
99+
return True, {}
100+
101+
register_storage_health_checks(hc, FakeStorage())
102+
assert hc.checks["database"].__name__ == "_check_storage_backend_health"
103+
104+
105+
# ── force flag on register_check ──────────────────────────────────────────
106+
107+
108+
def test_register_check_is_first_write_wins_without_force(tmp_path: Path) -> None:
109+
hc = _build_health_check(tmp_path)
110+
111+
def first() -> Any:
112+
return None
113+
114+
def second() -> Any:
115+
return None
116+
117+
hc.register_check("custom", first)
118+
hc.register_check("custom", second) # ignored
119+
assert hc.checks["custom"] is first
120+
121+
122+
def test_register_check_force_overwrites(tmp_path: Path) -> None:
123+
hc = _build_health_check(tmp_path)
124+
125+
def first() -> Any:
126+
return None
127+
128+
def second() -> Any:
129+
return None
130+
131+
hc.register_check("custom", first)
132+
hc.register_check("custom", second, force=True)
133+
assert hc.checks["custom"] is second
134+
135+
136+
# ── JSON strategy is_healthy ───────────────────────────────────────────────
137+
138+
139+
def test_json_strategy_is_healthy_for_fresh_install(tmp_path: Path) -> None:
140+
from orb.infrastructure.storage.json.strategy import JSONStorageStrategy
141+
142+
strategy = JSONStorageStrategy(
143+
file_path=str(tmp_path / "data.json"),
144+
create_dirs=True,
145+
entity_type="machines",
146+
)
147+
healthy, details = strategy.is_healthy()
148+
assert healthy is True
149+
assert details["state"] == "empty"
150+
assert details["entity_type"] == "machines"
151+
152+
153+
def test_json_strategy_is_healthy_with_records(tmp_path: Path) -> None:
154+
from orb.infrastructure.storage.json.strategy import JSONStorageStrategy
155+
156+
p = tmp_path / "data.json"
157+
p.write_text('{"id-1": {"name": "x", "status": "running"}}', encoding="utf-8")
158+
strategy = JSONStorageStrategy(
159+
file_path=str(p),
160+
create_dirs=True,
161+
entity_type="machines",
162+
)
163+
healthy, details = strategy.is_healthy()
164+
assert healthy is True
165+
assert details["entity_count"] == 1
166+
assert details["sample_keys"] == ["name", "status"]
167+
168+
169+
def test_json_strategy_unhealthy_on_malformed_file(tmp_path: Path) -> None:
170+
from orb.infrastructure.storage.json.strategy import JSONStorageStrategy
171+
172+
p = tmp_path / "data.json"
173+
p.write_text("not json at all", encoding="utf-8")
174+
strategy = JSONStorageStrategy(
175+
file_path=str(p),
176+
create_dirs=True,
177+
entity_type="machines",
178+
)
179+
healthy, details = strategy.is_healthy()
180+
assert healthy is False
181+
assert "error" in details
182+
183+
184+
def test_json_strategy_unhealthy_on_non_object_root(tmp_path: Path) -> None:
185+
from orb.infrastructure.storage.json.strategy import JSONStorageStrategy
186+
187+
p = tmp_path / "data.json"
188+
p.write_text("[1, 2, 3]", encoding="utf-8")
189+
strategy = JSONStorageStrategy(
190+
file_path=str(p),
191+
create_dirs=True,
192+
entity_type="machines",
193+
)
194+
healthy, details = strategy.is_healthy()
195+
assert healthy is False
196+
assert "expected JSON object" in details["reason"]
197+
198+
199+
def test_json_strategy_unhealthy_when_record_is_not_dict(tmp_path: Path) -> None:
200+
from orb.infrastructure.storage.json.strategy import JSONStorageStrategy
201+
202+
p = tmp_path / "data.json"
203+
p.write_text('{"id-1": "should-be-a-dict"}', encoding="utf-8")
204+
strategy = JSONStorageStrategy(
205+
file_path=str(p),
206+
create_dirs=True,
207+
entity_type="machines",
208+
)
209+
healthy, details = strategy.is_healthy()
210+
assert healthy is False
211+
assert "sampled record" in details["reason"]

tests/unit/test_health_no_aws.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,12 @@ def test_register_aws_health_checks_always_registers_aws_and_ec2():
6161
assert health_check.register_check.call_count == 2
6262

6363

64-
def test_register_aws_health_checks_registers_dynamodb_when_dynamodb_storage():
65-
"""register_aws_health_checks must add dynamodb check only when storage_strategy='dynamodb'."""
64+
def test_register_aws_health_checks_does_not_register_dynamodb_anymore():
65+
"""The dynamodb branch was removed: storage health now flows through
66+
register_storage_health_checks against the active StoragePort, so
67+
register_aws_health_checks no longer registers a 'database' or
68+
'dynamodb' check even when storage_strategy='dynamodb'.
69+
"""
6670
from unittest.mock import MagicMock
6771

6872
from orb.providers.aws.health import register_aws_health_checks
@@ -75,8 +79,9 @@ def test_register_aws_health_checks_registers_dynamodb_when_dynamodb_storage():
7579
registered_names = {call.args[0] for call in health_check.register_check.call_args_list}
7680
assert "aws" in registered_names
7781
assert "ec2" in registered_names
78-
assert "dynamodb" in registered_names
79-
assert health_check.register_check.call_count == 3
82+
assert "dynamodb" not in registered_names
83+
assert "database" not in registered_names
84+
assert health_check.register_check.call_count == 2
8085

8186

8287
def test_register_aws_health_checks_skips_dynamodb_for_sql_storage():

tests/unit/test_import_guards.py

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -73,28 +73,36 @@ def test_cli_main_without_rich_argparse(self):
7373

7474
def test_api_server_without_fastapi(self):
7575
"""Test API server gracefully fails without FastAPI."""
76-
with patch.dict(
77-
sys.modules,
78-
{
79-
"fastapi": None,
80-
"fastapi.middleware": None,
81-
"fastapi.middleware.cors": None,
82-
"fastapi.middleware.trustedhost": None,
83-
"fastapi.responses": None,
84-
},
85-
):
86-
# Force reimport to test guard
87-
if "orb.api.server" in sys.modules:
88-
del sys.modules["orb.api.server"]
89-
90-
from orb.api.server import create_fastapi_app
91-
92-
with pytest.raises(ImportError) as exc_info:
93-
create_fastapi_app(None)
94-
95-
error_msg = str(exc_info.value)
96-
assert "FastAPI not installed" in error_msg
97-
assert "pip install orb-py[api]" in error_msg
76+
_orig_server_mod = sys.modules.get("orb.api.server")
77+
try:
78+
with patch.dict(
79+
sys.modules,
80+
{
81+
"fastapi": None,
82+
"fastapi.middleware": None,
83+
"fastapi.middleware.cors": None,
84+
"fastapi.middleware.trustedhost": None,
85+
"fastapi.responses": None,
86+
},
87+
):
88+
# Force reimport to test guard
89+
if "orb.api.server" in sys.modules:
90+
del sys.modules["orb.api.server"]
91+
92+
from orb.api.server import create_fastapi_app
93+
94+
with pytest.raises(ImportError) as exc_info:
95+
create_fastapi_app(None)
96+
97+
error_msg = str(exc_info.value)
98+
assert "FastAPI not installed" in error_msg
99+
assert "pip install orb-py[api]" in error_msg
100+
finally:
101+
if _orig_server_mod is not None:
102+
import orb.api
103+
104+
setattr(orb.api, "server", _orig_server_mod)
105+
sys.modules["orb.api.server"] = _orig_server_mod
98106

99107
def test_monitoring_without_optional_deps(self):
100108
"""Test monitoring works without optional dependencies."""
@@ -209,18 +217,26 @@ class TestErrorMessages:
209217

210218
def test_api_error_message_helpful(self):
211219
"""Test API error message tells user how to install."""
212-
with patch.dict(sys.modules, {"fastapi": None}):
213-
if "orb.api.server" in sys.modules:
214-
del sys.modules["orb.api.server"]
215-
216-
from orb.api.server import create_fastapi_app
217-
218-
with pytest.raises(ImportError) as exc_info:
219-
create_fastapi_app(None)
220-
221-
error_msg = str(exc_info.value)
222-
assert "FastAPI not installed" in error_msg
223-
assert "pip install orb-py[api]" in error_msg
220+
_orig_server_mod = sys.modules.get("orb.api.server")
221+
try:
222+
with patch.dict(sys.modules, {"fastapi": None}):
223+
if "orb.api.server" in sys.modules:
224+
del sys.modules["orb.api.server"]
225+
226+
from orb.api.server import create_fastapi_app
227+
228+
with pytest.raises(ImportError) as exc_info:
229+
create_fastapi_app(None)
230+
231+
error_msg = str(exc_info.value)
232+
assert "FastAPI not installed" in error_msg
233+
assert "pip install orb-py[api]" in error_msg
234+
finally:
235+
if _orig_server_mod is not None:
236+
import orb.api
237+
238+
setattr(orb.api, "server", _orig_server_mod)
239+
sys.modules["orb.api.server"] = _orig_server_mod
224240

225241
def test_serve_command_error_message(self):
226242
"""Test serve command error message is helpful."""

0 commit comments

Comments
 (0)