164 lines
5.1 KiB
Python
164 lines
5.1 KiB
Python
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'scripts'))
|
|
|
|
from sovereign_dns import (
|
|
CloudflareDNSProvider,
|
|
Route53DNSProvider,
|
|
build_sync_plan,
|
|
detect_public_ip,
|
|
load_domain_mapping,
|
|
resolve_domain_ip_map,
|
|
)
|
|
|
|
|
|
def test_load_domain_mapping_reads_ansible_style_domain_to_ip_map(tmp_path):
|
|
cfg = tmp_path / 'dns_records.yaml'
|
|
cfg.write_text(
|
|
"""
|
|
dns_provider: cloudflare
|
|
dns_zone_id: zone-123
|
|
domain_ip_map:
|
|
forge.example.com: 1.2.3.4
|
|
matrix.example.com: 5.6.7.8
|
|
"""
|
|
)
|
|
|
|
loaded = load_domain_mapping(cfg)
|
|
|
|
assert loaded['dns_provider'] == 'cloudflare'
|
|
assert loaded['dns_zone_id'] == 'zone-123'
|
|
assert loaded['domain_ip_map'] == {
|
|
'forge.example.com': '1.2.3.4',
|
|
'matrix.example.com': '5.6.7.8',
|
|
}
|
|
|
|
|
|
def test_build_sync_plan_updates_changed_ip_and_creates_missing_records():
|
|
current = {
|
|
'forge.example.com': {'id': 'rec-1', 'content': '1.1.1.1'},
|
|
'old.example.com': {'id': 'rec-2', 'content': '9.9.9.9'},
|
|
}
|
|
desired = {
|
|
'forge.example.com': '2.2.2.2',
|
|
'new.example.com': '3.3.3.3',
|
|
}
|
|
|
|
plan = build_sync_plan(current=current, desired=desired)
|
|
|
|
assert plan['update'] == [
|
|
{'name': 'forge.example.com', 'id': 'rec-1', 'content': '2.2.2.2'}
|
|
]
|
|
assert plan['create'] == [
|
|
{'name': 'new.example.com', 'content': '3.3.3.3'}
|
|
]
|
|
assert plan['delete'] == [
|
|
{'name': 'old.example.com', 'id': 'rec-2'}
|
|
]
|
|
|
|
|
|
def test_resolve_domain_ip_map_replaces_auto_values_with_detected_public_ip():
|
|
resolved = resolve_domain_ip_map(
|
|
{
|
|
'forge.example.com': 'auto',
|
|
'matrix.example.com': '5.6.7.8',
|
|
},
|
|
current_public_ip='8.8.4.4',
|
|
)
|
|
|
|
assert resolved == {
|
|
'forge.example.com': '8.8.4.4',
|
|
'matrix.example.com': '5.6.7.8',
|
|
}
|
|
|
|
|
|
def test_detect_public_ip_reads_provider_response():
|
|
class FakeResponse:
|
|
def __enter__(self):
|
|
return self
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
def read(self):
|
|
return b'4.3.2.1\n'
|
|
|
|
ip = detect_public_ip(lambda req, timeout=10: FakeResponse())
|
|
assert ip == '4.3.2.1'
|
|
|
|
|
|
def test_cloudflare_upsert_calls_expected_http_methods():
|
|
calls = []
|
|
|
|
def fake_request(method, path, payload=None):
|
|
calls.append({'method': method, 'path': path, 'payload': payload})
|
|
if method == 'GET':
|
|
return {'success': True, 'result': []}
|
|
return {'success': True, 'result': {'id': 'created-id'}}
|
|
|
|
provider = CloudflareDNSProvider(
|
|
api_token='tok',
|
|
zone_id='zone-1',
|
|
request_fn=fake_request,
|
|
)
|
|
|
|
provider.upsert_a_record('forge.example.com', '1.2.3.4')
|
|
|
|
assert calls[0]['method'] == 'GET'
|
|
assert calls[0]['path'] == '/zones/zone-1/dns_records?type=A&name=forge.example.com'
|
|
assert calls[1]['method'] == 'POST'
|
|
assert calls[1]['path'] == '/zones/zone-1/dns_records'
|
|
assert calls[1]['payload']['name'] == 'forge.example.com'
|
|
assert calls[1]['payload']['content'] == '1.2.3.4'
|
|
assert calls[1]['payload']['type'] == 'A'
|
|
|
|
|
|
def test_cloudflare_upsert_updates_when_record_exists():
|
|
calls = []
|
|
|
|
def fake_request(method, path, payload=None):
|
|
calls.append({'method': method, 'path': path, 'payload': payload})
|
|
if method == 'GET':
|
|
return {'success': True, 'result': [{'id': 'rec-123', 'content': '1.1.1.1'}]}
|
|
return {'success': True, 'result': {'id': 'rec-123'}}
|
|
|
|
provider = CloudflareDNSProvider(
|
|
api_token='tok',
|
|
zone_id='zone-1',
|
|
request_fn=fake_request,
|
|
)
|
|
|
|
provider.upsert_a_record('forge.example.com', '2.2.2.2')
|
|
|
|
assert calls[1]['method'] == 'PUT'
|
|
assert calls[1]['path'] == '/zones/zone-1/dns_records/rec-123'
|
|
assert calls[1]['payload']['content'] == '2.2.2.2'
|
|
|
|
|
|
def test_route53_sync_uses_change_batches():
|
|
batches = []
|
|
|
|
class FakeClient:
|
|
def change_resource_record_sets(self, HostedZoneId, ChangeBatch):
|
|
batches.append({'HostedZoneId': HostedZoneId, 'ChangeBatch': ChangeBatch})
|
|
return {'ChangeInfo': {'Status': 'PENDING'}}
|
|
|
|
provider = Route53DNSProvider(hosted_zone_id='ZONE123', client=FakeClient())
|
|
provider.apply_plan(
|
|
create=[{'name': 'new.example.com', 'content': '3.3.3.3'}],
|
|
update=[{'name': 'forge.example.com', 'id': 'ignored', 'content': '2.2.2.2'}],
|
|
delete=[{'name': 'old.example.com', 'id': 'ignored'}],
|
|
current={'old.example.com': {'content': '9.9.9.9'}},
|
|
)
|
|
|
|
batch = batches[0]
|
|
assert batch['HostedZoneId'] == 'ZONE123'
|
|
changes = batch['ChangeBatch']['Changes']
|
|
assert changes[0]['Action'] == 'CREATE'
|
|
assert changes[0]['ResourceRecordSet']['Name'] == 'new.example.com'
|
|
assert changes[1]['Action'] == 'UPSERT'
|
|
assert changes[1]['ResourceRecordSet']['Name'] == 'forge.example.com'
|
|
assert changes[2]['Action'] == 'DELETE'
|
|
assert changes[2]['ResourceRecordSet']['Name'] == 'old.example.com'
|