FluxIP/tests/test_cloud_clients.py

279 lines
9.7 KiB
Python

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"