mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-02 22:06:00 +01:00
- Reworked many tests - Enforcing lint + type checking for test suite - Fixed various issues surfaced by the new tests - CI tweaks
276 lines
8.9 KiB
Python
276 lines
8.9 KiB
Python
import pytest
|
|
from _pytest.outcomes import Failed, Skipped
|
|
|
|
from tests.e2e import conftest as e2e_conftest
|
|
|
|
|
|
class DummyResponse:
|
|
def __init__(self, status_code: int, payload: object, text: str = "") -> None:
|
|
self.status_code = status_code
|
|
self._payload = payload
|
|
self.text = text or repr(payload)
|
|
|
|
def json(self) -> object:
|
|
return self._payload
|
|
|
|
|
|
class RaisingResponse(DummyResponse):
|
|
def __init__(self, status_code: int, exc: Exception, text: str = "") -> None:
|
|
super().__init__(status_code=status_code, payload=None, text=text)
|
|
self._exc = exc
|
|
|
|
def json(self) -> object:
|
|
raise self._exc
|
|
|
|
|
|
def _make_client() -> e2e_conftest.APIClient:
|
|
return e2e_conftest.APIClient(base_url="http://example.com")
|
|
|
|
|
|
def test_api_client_wait_for_health_retries_through_request_exception(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = _make_client()
|
|
calls = {"count": 0}
|
|
|
|
def fake_get(_path: str, **_kwargs: object) -> DummyResponse:
|
|
calls["count"] += 1
|
|
if calls["count"] == 1:
|
|
raise e2e_conftest.requests.exceptions.ReadTimeout("timeout")
|
|
return DummyResponse(200, {"status": "ok"})
|
|
|
|
times = [0.0, 0.0, 0.1]
|
|
|
|
monkeypatch.setattr(client, "get", fake_get)
|
|
monkeypatch.setattr(e2e_conftest.time, "time", lambda: times.pop(0))
|
|
monkeypatch.setattr(e2e_conftest.time, "sleep", lambda _seconds: None)
|
|
|
|
try:
|
|
assert client.wait_for_health(max_wait=1) is True
|
|
assert calls["count"] == 2
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_download_tracker_wait_for_status_ignores_malformed_payloads_and_returns_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class FakeClient:
|
|
def __init__(self) -> None:
|
|
self.responses = [
|
|
DummyResponse(200, ["not", "a", "mapping"]),
|
|
DummyResponse(200, {"error": {"task-1": {"message": "boom"}}}),
|
|
]
|
|
|
|
def get(self, _path: str) -> DummyResponse:
|
|
return self.responses.pop(0)
|
|
|
|
tracker = e2e_conftest.DownloadTracker(client=FakeClient())
|
|
times = [0.0, 0.0, 0.1]
|
|
|
|
monkeypatch.setattr(e2e_conftest.time, "time", lambda: times.pop(0))
|
|
monkeypatch.setattr(e2e_conftest.time, "sleep", lambda _seconds: None)
|
|
|
|
result = tracker.wait_for_status("task-1", ["complete"], timeout=1)
|
|
|
|
assert result == {"state": "error", "data": {"message": "boom"}}
|
|
|
|
|
|
def test_assert_json_object_returns_dict() -> None:
|
|
response = DummyResponse(200, {"status": "ok"})
|
|
|
|
assert e2e_conftest.assert_json_object(response, context="health") == {"status": "ok"}
|
|
|
|
|
|
def test_assert_json_object_rejects_non_object_payload() -> None:
|
|
response = DummyResponse(200, ["not", "an", "object"])
|
|
|
|
with pytest.raises(AssertionError, match="did not return a JSON object"):
|
|
e2e_conftest.assert_json_object(response, context="health")
|
|
|
|
|
|
def test_assert_json_object_rejects_invalid_json() -> None:
|
|
response = RaisingResponse(200, ValueError("bad json"))
|
|
|
|
with pytest.raises(Failed, match="did not return valid JSON"):
|
|
e2e_conftest.assert_json_object(response, context="health")
|
|
|
|
|
|
def test_assert_json_list_returns_list() -> None:
|
|
response = DummyResponse(200, [1, 2, 3])
|
|
|
|
assert e2e_conftest.assert_json_list(response, context="queue") == [1, 2, 3]
|
|
|
|
|
|
def test_assert_queue_order_response_validates_entries() -> None:
|
|
response = DummyResponse(
|
|
200,
|
|
{
|
|
"queue": [
|
|
{
|
|
"id": "task-1",
|
|
"priority": 0,
|
|
"added_time": 12.5,
|
|
"status": "queued",
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
queue = e2e_conftest.assert_queue_order_response(response)
|
|
assert queue[0]["id"] == "task-1"
|
|
|
|
|
|
def test_assert_queue_order_response_rejects_missing_entry_fields() -> None:
|
|
response = DummyResponse(200, {"queue": [{"id": "task-1"}]})
|
|
|
|
with pytest.raises(AssertionError):
|
|
e2e_conftest.assert_queue_order_response(response)
|
|
|
|
|
|
def test_require_authenticated_client_allows_public_server(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = _make_client()
|
|
monkeypatch.setattr(e2e_conftest, "_get_auth_state", lambda _: {"auth_required": False})
|
|
|
|
try:
|
|
assert e2e_conftest._require_authenticated_client(client, strict=True) is client
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_require_authenticated_client_fails_when_auth_state_unavailable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = _make_client()
|
|
monkeypatch.setattr(e2e_conftest, "_get_auth_state", lambda _: None)
|
|
|
|
try:
|
|
with pytest.raises(Failed, match="Unable to read auth state"):
|
|
e2e_conftest._require_authenticated_client(client, strict=True)
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_require_healthy_server_fails_in_strict_runs(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
closed: list[str] = []
|
|
|
|
monkeypatch.setattr(e2e_conftest.APIClient, "wait_for_health", lambda self, max_wait=30: False)
|
|
monkeypatch.setattr(e2e_conftest.APIClient, "close", lambda self: closed.append(self.base_url))
|
|
|
|
with pytest.raises(Failed, match="Server not available"):
|
|
e2e_conftest._require_healthy_server("http://example.com", strict=True)
|
|
|
|
assert closed == ["http://example.com"]
|
|
|
|
|
|
def test_require_healthy_server_skips_in_non_strict_runs(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(e2e_conftest.APIClient, "wait_for_health", lambda self, max_wait=30: False)
|
|
|
|
with pytest.raises(Skipped, match="Server not available"):
|
|
e2e_conftest._require_healthy_server("http://example.com", strict=False)
|
|
|
|
|
|
def test_require_authenticated_client_skips_when_auth_state_unavailable_and_not_strict(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = _make_client()
|
|
monkeypatch.setattr(e2e_conftest, "_get_auth_state", lambda _: None)
|
|
|
|
try:
|
|
with pytest.raises(Skipped, match="Unable to read auth state"):
|
|
e2e_conftest._require_authenticated_client(client, strict=False)
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_require_authenticated_client_fails_without_env_credentials(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = _make_client()
|
|
monkeypatch.setattr(
|
|
e2e_conftest,
|
|
"_get_auth_state",
|
|
lambda _: {"auth_required": True, "authenticated": False},
|
|
)
|
|
monkeypatch.delenv(e2e_conftest.E2E_USERNAME_ENV, raising=False)
|
|
monkeypatch.delenv(e2e_conftest.E2E_PASSWORD_ENV, raising=False)
|
|
|
|
try:
|
|
with pytest.raises(Failed, match="requires authentication"):
|
|
e2e_conftest._require_authenticated_client(client, strict=True)
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_require_authenticated_client_skips_without_env_credentials_when_not_strict(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = _make_client()
|
|
monkeypatch.setattr(
|
|
e2e_conftest,
|
|
"_get_auth_state",
|
|
lambda _: {"auth_required": True, "authenticated": False},
|
|
)
|
|
monkeypatch.delenv(e2e_conftest.E2E_USERNAME_ENV, raising=False)
|
|
monkeypatch.delenv(e2e_conftest.E2E_PASSWORD_ENV, raising=False)
|
|
|
|
try:
|
|
with pytest.raises(Skipped, match="requires authentication"):
|
|
e2e_conftest._require_authenticated_client(client, strict=False)
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_require_authenticated_client_logs_in_when_credentials_are_available(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = _make_client()
|
|
auth_states = iter(
|
|
[
|
|
{"auth_required": True, "authenticated": False},
|
|
{"auth_required": True, "authenticated": True},
|
|
]
|
|
)
|
|
monkeypatch.setattr(e2e_conftest, "_get_auth_state", lambda _: next(auth_states))
|
|
monkeypatch.setattr(e2e_conftest, "_login_with_env_credentials", lambda _: True)
|
|
monkeypatch.setenv(e2e_conftest.E2E_USERNAME_ENV, "user")
|
|
monkeypatch.setenv(e2e_conftest.E2E_PASSWORD_ENV, "password")
|
|
|
|
try:
|
|
assert e2e_conftest._require_authenticated_client(client, strict=True) is client
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def test_is_explicit_e2e_run_detects_e2e_path_selection() -> None:
|
|
assert e2e_conftest._is_explicit_e2e_run("", ["tests/e2e/"]) is True
|
|
assert (
|
|
e2e_conftest._is_explicit_e2e_run("", ["tests/e2e/test_api.py::TestConfigEndpoint"]) is True
|
|
)
|
|
|
|
|
|
def test_is_explicit_e2e_run_detects_mixed_path_selection() -> None:
|
|
assert (
|
|
e2e_conftest._is_explicit_e2e_run(
|
|
"",
|
|
["tests/e2e/test_api.py", "tests/core/test_user_db.py"],
|
|
)
|
|
is True
|
|
)
|
|
|
|
|
|
def test_is_explicit_e2e_run_detects_markexpr_selection() -> None:
|
|
assert e2e_conftest._is_explicit_e2e_run("e2e", ["tests/"]) is True
|
|
assert e2e_conftest._is_explicit_e2e_run("slow and e2e", ["tests/"]) is True
|
|
|
|
|
|
def test_is_explicit_e2e_run_treats_general_suite_as_non_strict() -> None:
|
|
assert e2e_conftest._is_explicit_e2e_run("", ["tests/"]) is False
|
|
assert e2e_conftest._is_explicit_e2e_run("not integration and not e2e", ["tests/"]) is False
|