from __future__ import annotations from typing import Any import httpx import pytest import app.integrations.cloudflare_client as cloudflare_module from app.core.errors import ExternalServiceError, ValidationAppError from app.integrations.cloudflare_client import CloudflareClient class FakeHttpxClient: responses: list[httpx.Response | Exception] = [] requests: list[tuple[str, str, dict[str, Any]]] = [] def __init__(self, **_kwargs: Any) -> None: pass def __enter__(self) -> FakeHttpxClient: return self def __exit__(self, *_args: object) -> None: return None def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response: self.requests.append((method, path, kwargs)) item = self.responses.pop(0) if isinstance(item, Exception): raise item return item def json_response(status: int, payload: dict[str, object]) -> httpx.Response: return httpx.Response( status, json=payload, request=httpx.Request("GET", "https://api.cloudflare.test/resource"), ) def test_cloudflare_request_maps_success_and_upstream_failures( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(cloudflare_module.httpx, "Client", FakeHttpxClient) FakeHttpxClient.requests = [] client = CloudflareClient( token="token", zone_name="example.com", record_name="socks.example.com" ) FakeHttpxClient.responses = [json_response(200, {"success": True, "result": {"id": "1"}})] assert client._request("GET", "/ok") == {"id": "1"} assert FakeHttpxClient.requests[-1][:2] == ("GET", "/ok") FakeHttpxClient.responses = [ json_response( 403, {"success": False, "errors": [{"code": 9109, "message": "invalid token"}]}, ) ] with pytest.raises(ExternalServiceError, match="invalid token") as api_error: client._request("GET", "/denied") assert api_error.value.code == "9109" FakeHttpxClient.responses = [ httpx.Response( 200, content=b"not-json", request=httpx.Request("GET", "https://api.cloudflare.test/invalid"), ) ] with pytest.raises(ExternalServiceError) as invalid_response: client._request("GET", "/invalid") assert invalid_response.value.code == "CLOUDFLARE_INVALID_RESPONSE" FakeHttpxClient.responses = [ httpx.ConnectError( "offline", request=httpx.Request("GET", "https://api.cloudflare.test/offline") ) ] with pytest.raises(ExternalServiceError) as connection_error: client._request("GET", "/offline") assert connection_error.value.code == "CLOUDFLARE_CONNECTION_FAILED" def test_cloudflare_dns_contract_rejects_conflicts_and_upserts_dns_only( monkeypatch: pytest.MonkeyPatch, ) -> None: records: list[dict[str, object]] = [ { "id": "record-id", "name": "socks.example.com", "type": "A", "content": "198.51.100.10", "proxied": False, "ttl": 60, } ] calls: list[tuple[str, str, dict[str, Any]]] = [] def fake_request(method: str, path: str, **kwargs: Any) -> object: calls.append((method, path, kwargs)) if path == "/zones": return [{"id": "resolved-zone", "name": "example.com"}] if path == "/zones/resolved-zone": return {"id": "resolved-zone", "name": "example.com"} if path.endswith("/dns_records") and method == "GET": return [*records, {"id": "ignored", "name": "other.example.com", "type": "A"}] if "/dns_records/record-id" in path and method == "PATCH": records[0] = {"id": "record-id", **kwargs["json"]} return dict(records[0]) if path.endswith("/dns_records") and method == "POST": created = {"id": "created-id", **kwargs["json"]} records.append(created) return created raise AssertionError(f"unexpected Cloudflare request: {method} {path}") client = CloudflareClient( token="token", zone_name="example.com", record_name="socks.example.com" ) monkeypatch.setattr(client, "_request", fake_request) status = client.test_connection() assert status["zone_id"] == "resolved-zone" assert status["record_ip"] == "198.51.100.10" assert status["proxied"] is False assert status["expected_proxied"] is False updated = client.upsert_a_record("198.51.100.20", expected_current="198.51.100.10") assert updated["content"] == "198.51.100.20" assert updated["proxied"] is False assert updated["ttl"] == 60 assert any(method == "PATCH" for method, _path, _kwargs in calls) with pytest.raises(ValidationAppError) as externally_modified: client.upsert_a_record("198.51.100.30", expected_current="198.51.100.10") assert externally_modified.value.code == "DNS_EXTERNALLY_MODIFIED" with pytest.raises(ValidationAppError) as unexpectedly_present: client.upsert_a_record("198.51.100.30", expected_current=None) assert unexpectedly_present.value.code == "DNS_EXTERNALLY_MODIFIED" unconstrained = client.upsert_a_record("198.51.100.30") assert unconstrained["content"] == "198.51.100.30" idempotent = client.upsert_a_record("198.51.100.30", expected_current=None) assert idempotent["content"] == "198.51.100.30" records.clear() with pytest.raises(ValidationAppError) as externally_deleted: client.upsert_a_record("198.51.100.40", expected_current="198.51.100.30") assert externally_deleted.value.code == "DNS_EXTERNALLY_MODIFIED" created = client.upsert_a_record("198.51.100.40", expected_current=None) assert created["id"] == "created-id" with pytest.raises(ValidationAppError) as conflict: CloudflareClient._validate_records( [{"type": "AAAA", "name": "socks.example.com", "content": "::1"}] ) assert conflict.value.code == "CONFLICTING_DNS_RECORD" with pytest.raises(ValidationAppError) as duplicates: CloudflareClient._validate_records( [ {"type": "A", "content": "198.51.100.1"}, {"type": "A", "content": "198.51.100.2"}, ] ) assert duplicates.value.code == "MULTIPLE_A_RECORDS" assert any(method == "POST" for method, _path, _kwargs in calls) def test_cloudflare_upsert_supports_proxied_records_with_automatic_ttl( monkeypatch: pytest.MonkeyPatch, ) -> None: record = { "id": "record-id", "name": "socks.example.com", "type": "A", "content": "198.51.100.10", "proxied": False, "ttl": 60, } written_bodies: list[dict[str, object]] = [] def fake_request(method: str, path: str, **kwargs: Any) -> object: if path == "/zones/zone-id": return {"id": "zone-id", "name": "example.com"} if path.endswith("/dns_records") and method == "GET": return [dict(record)] if path.endswith("/dns_records/record-id") and method == "PATCH": written_bodies.append(dict(kwargs["json"])) written = {"id": "record-id", **kwargs["json"]} record.update(written) return written raise AssertionError(f"unexpected Cloudflare request: {method} {path}") client = CloudflareClient( token="token", zone_name="example.com", zone_id="zone-id", record_name="socks.example.com", proxied=True, ) monkeypatch.setattr(client, "_request", fake_request) before = client.test_connection() assert before["proxied"] is False assert before["expected_proxied"] is True updated = client.upsert_a_record( "198.51.100.20", expected_current="198.51.100.10", ) assert written_bodies == [ { "type": "A", "name": "socks.example.com", "content": "198.51.100.20", "ttl": 1, "proxied": True, } ] assert updated["content"] == "198.51.100.20" assert updated["proxied"] is True assert updated["ttl"] == 1 after = client.test_connection() assert after["proxied"] is True assert after["expected_proxied"] is True assert after["ttl"] == 1 @pytest.mark.parametrize( ("mutated_field", "mutated_value"), [ ("content", "198.51.100.99"), ("proxied", False), ("ttl", 60), ], ) def test_cloudflare_upsert_rejects_failed_post_write_verification( monkeypatch: pytest.MonkeyPatch, mutated_field: str, mutated_value: object, ) -> None: record = { "id": "record-id", "name": "socks.example.com", "type": "A", "content": "198.51.100.10", "proxied": True, "ttl": 1, } def fake_request(method: str, path: str, **kwargs: Any) -> object: if path == "/zones/zone-id": return {"id": "zone-id", "name": "example.com"} if path.endswith("/dns_records") and method == "GET": return [dict(record)] if path.endswith("/dns_records/record-id") and method == "PATCH": written = {"id": "record-id", **kwargs["json"]} record.update(written) record[mutated_field] = mutated_value return written raise AssertionError(f"unexpected Cloudflare request: {method} {path}") client = CloudflareClient( token="token", zone_name="example.com", zone_id="zone-id", record_name="socks.example.com", proxied=True, ) monkeypatch.setattr(client, "_request", fake_request) with pytest.raises(ValidationAppError) as verification_error: client.upsert_a_record("198.51.100.20", expected_current="198.51.100.10") assert verification_error.value.code == "DNS_WRITE_NOT_VERIFIED"