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:52:32 +00:00
parent 0997417523
commit bcc64ea7ca

View File

@@ -1,92 +1,71 @@
import json
import logging import logging
import httpx import httpx
import pytest import pytest
from fastapi.testclient import TestClient
from app.checker import EndpointChecker from endpoint_monitor.checker import EndpointChecker, TargetBlockedError, validate_target
from app.config import Settings from endpoint_monitor.logging import JsonFormatter, redact_url
from app.logging import JsonFormatter
from app.main import app
from app.models import State
from app.security import DestinationRejected, redacted_url
async def public_resolver(host: str, port: int) -> list[str]: async def public_resolver(host: str, port: int) -> list[str]:
return ["93.184.216.34"] return ["93.184.216.34"]
async def private_resolver(host: str, port: int) -> list[str]: @pytest.mark.parametrize("address", ["127.0.0.1", "10.0.0.1", "169.254.169.254", "::1", "fc00::1"])
return ["127.0.0.1"] async def test_dns_answers_block_non_public(address):
async def resolver(host: str, port: int) -> list[str]:
return [address]
with pytest.raises(TargetBlockedError, match="non-public"):
await validate_target("http://attacker.example/path", resolver)
@pytest.mark.asyncio async def test_any_unsafe_dns_answer_blocks_target():
async def test_success_down_and_transport_error() -> None: async def resolver(host: str, port: int) -> list[str]:
async def handler(request: httpx.Request) -> httpx.Response: return ["93.184.216.34", "127.0.0.1"]
if request.url.path == "/ok":
return httpx.Response(204) with pytest.raises(TargetBlockedError):
if request.url.path == "/down": await validate_target("https://mixed.example", resolver)
async def test_redirect_hop_is_resolved_and_blocked():
seen = []
async def resolver(host: str, port: int) -> list[str]:
seen.append(host)
return ["127.0.0.1"] if host == "internal.example" else ["93.184.216.34"]
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(302, headers={"location": "http://internal.example/admin"})
checker = EndpointChecker(1, 3, resolver, httpx.MockTransport(handler))
with pytest.raises(TargetBlockedError):
await checker.check("https://public.example/start", "id")
assert seen == ["public.example", "internal.example"]
async def test_success_latency_status_and_redaction(caplog):
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(503) return httpx.Response(503)
raise httpx.ConnectError("secret host detail", request=request)
worker = EndpointChecker(Settings(), public_resolver, httpx.MockTransport(handler)) checker = EndpointChecker(1, 1, public_resolver, httpx.MockTransport(handler))
assert (await worker.check("1", "https://example.com/ok?token=x")).state is State.UP with caplog.at_level(logging.INFO):
assert (await worker.check("1", "https://example.com/down")).state is State.DOWN result = await checker.check("https://example.com/path?token=supersecret#fragment", "abc")
failed = await worker.check("1", "https://example.com/fail") assert result.status.value == "down"
assert failed.state is State.ERROR assert result.http_status == 503
assert failed.error == "outbound request failed: ConnectError" assert result.latency_ms >= 0
assert result.final_url == "https://example.com/path"
assert "supersecret" not in caplog.text
assert "fragment" not in caplog.text
@pytest.mark.asyncio def test_redact_url_removes_credentials_query_fragment():
async def test_private_dns_and_private_redirect_are_blocked() -> None: assert redact_url("https://user:pass@example.com:8443/a?q=secret#x") == "https://example.com:8443/a"
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_api_check_updates_status_and_rejection(client: TestClient) -> None: def test_json_formatter_is_structured_and_does_not_add_message_secrets():
created = client.post( record = logging.LogRecord("x", logging.INFO, __file__, 1, "event", (), None)
"/v1/monitors", json={"name": "site", "url": "https://example.com/ok"} record.url = redact_url("https://example.com/?api_key=secret")
).json() rendered = JsonFormatter().format(record)
app.state.checker = EndpointChecker( assert '"event":"event"' in rendered
Settings(), public_resolver, httpx.MockTransport(lambda r: httpx.Response(200)) assert "secret" not in rendered
)
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))
)
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>")