decomposer: generate deliverable files for Define the service contract and project architecture for the FastAPI endpoint monitoring service.; Implement the typed monitor CRUD API and concurrency-safe in-memory state according to the service design.; Implement secure on-demand endpoint checks with status updates, latency measurement, robust error handling, and redacted structured logs.; Add operational API endpoints and environment-driven runtime configuration to the monitoring service.; Create automated tests for the monitoring service.; Package the service with Docker and developer documentation.; Validate the complete project.
Some checks failed
ci / validate (push) Has been cancelled

This commit is contained in:
2026-08-09 15:46:55 +00:00
parent 9453784868
commit ccb748a367

View File

@@ -1,72 +1,92 @@
import asyncio
import json
import logging
import socket
import httpx
import pytest
from fastapi.testclient import TestClient
from app.checker import SafeResolver, UnsafeTargetError
from app.logging_config import JsonFormatter, redact_url
from app.models import CurrentStatus, MonitorInput, State
from app.store import MonitorStore, StaleCheckError
from app.checker import EndpointChecker
from app.config import Settings
from app.logging import JsonFormatter
from app.main import app
from app.models import State
from app.security import DestinationRejected, redacted_url
def test_resolver_blocks_private_answer(monkeypatch: pytest.MonkeyPatch) -> None:
def private(*args: object, **kwargs: object) -> list[tuple]:
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", 80))]
monkeypatch.setattr(socket, "getaddrinfo", private)
with pytest.raises(UnsafeTargetError):
asyncio.run(SafeResolver().validate_url("http://attacker.example/path"))
async def public_resolver(host: str, port: int) -> list[str]:
return ["93.184.216.34"]
def test_resolver_accepts_only_public_answers(monkeypatch: pytest.MonkeyPatch) -> None:
def public(*args: object, **kwargs: object) -> list[tuple]:
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 443))]
monkeypatch.setattr(socket, "getaddrinfo", public)
results = asyncio.run(SafeResolver().resolve("example.com", 443, socket.AF_UNSPEC))
assert results[0]["host"] == "93.184.216.34"
async def private_resolver(host: str, port: int) -> list[str]:
return ["127.0.0.1"]
def test_redirect_destination_is_revalidated() -> None:
resolver = SafeResolver()
calls: list[str] = []
@pytest.mark.asyncio
async def test_success_down_and_transport_error() -> None:
async def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/ok":
return httpx.Response(204)
if request.url.path == "/down":
return httpx.Response(503)
raise httpx.ConnectError("secret host detail", request=request)
async def validate(url: str) -> None:
calls.append(url)
if len(calls) == 2:
raise UnsafeTargetError
resolver.validate_url = validate # type: ignore[method-assign]
async def redirects() -> None:
await resolver.validate_url("https://public.example")
with pytest.raises(UnsafeTargetError):
await resolver.validate_url("http://127.0.0.1/admin")
asyncio.run(redirects())
assert calls == ["https://public.example", "http://127.0.0.1/admin"]
worker = EndpointChecker(Settings(), public_resolver, httpx.MockTransport(handler))
assert (await worker.check("1", "https://example.com/ok?token=x")).state is State.UP
assert (await worker.check("1", "https://example.com/down")).state is State.DOWN
failed = await worker.check("1", "https://example.com/fail")
assert failed.state is State.ERROR
assert failed.error == "outbound request failed: ConnectError"
def test_url_and_structured_log_redaction() -> None:
secret = "https://user:pass@example.com/path?token=secret#fragment"
assert redact_url(secret) == "https://example.com/path"
formatter = JsonFormatter()
record = logging.LogRecord("test", logging.INFO, "", 0, "event", (), None)
record.fields = {"url": secret, "monitor_id": "abc"} # type: ignore[attr-defined]
payload = formatter.format(record)
assert "secret" not in payload and "pass" not in payload
assert json.loads(payload)["url"] == "https://example.com/path"
@pytest.mark.asyncio
async def test_private_dns_and_private_redirect_are_blocked() -> None:
worker = EndpointChecker(Settings(), private_resolver, httpx.MockTransport(lambda r: httpx.Response(200)))
with pytest.raises(DestinationRejected):
await worker.check("1", "http://example.com")
calls = 0
async def changing_resolver(host: str, port: int) -> list[str]:
return ["127.0.0.1"] if host == "internal.test" else ["93.184.216.34"]
async def redirect(request: httpx.Request) -> httpx.Response:
nonlocal calls
calls += 1
return httpx.Response(302, headers={"location": "http://internal.test/admin"})
worker = EndpointChecker(Settings(), changing_resolver, httpx.MockTransport(redirect))
with pytest.raises(DestinationRejected):
await worker.check("1", "https://example.com/start")
assert calls == 1
def test_stale_check_cannot_overwrite_new_url() -> None:
async def scenario() -> None:
store = MonitorStore(5)
item = await store.create(MonitorInput(name="x", url="https://example.com"))
await store.replace(
item.id, MonitorInput(name="x", url="https://example.org")
def test_api_check_updates_status_and_rejection(client: TestClient) -> None:
created = client.post(
"/v1/monitors", json={"name": "site", "url": "https://example.com/ok"}
).json()
app.state.checker = EndpointChecker(
Settings(), public_resolver, httpx.MockTransport(lambda r: httpx.Response(200))
)
with pytest.raises(StaleCheckError):
await store.publish_status(
item.id, str(item.url), CurrentStatus(state=State.UP)
response = client.post(f"/v1/monitors/{created['id']}/check")
assert response.status_code == 200
assert response.json()["state"] == "up"
assert client.get(f"/v1/monitors/{created['id']}/status").json()["status_code"] == 200
app.state.checker = EndpointChecker(
Settings(), private_resolver, httpx.MockTransport(lambda r: httpx.Response(200))
)
asyncio.run(scenario())
response = client.post(f"/v1/monitors/{created['id']}/check")
assert response.status_code == 400
assert client.get(f"/v1/monitors/{created['id']}/status").json()["state"] == "error"
def test_url_and_json_log_redaction() -> None:
safe = redacted_url("https://user:pass@example.com/a?token=secret#fragment")
assert safe == "https://example.com/a?<redacted>"
record = logging.LogRecord("x", logging.INFO, "", 0, "done", (), None)
record.url = safe
payload = json.loads(JsonFormatter().format(record))
serialized = json.dumps(payload)
assert "secret" not in serialized
assert "pass" not in serialized
assert payload["url"].endswith("?<redacted>")