Archived
Prepare KarlDNS production deployment
This commit is contained in:
1 parent
5a90bd9d87
commit
ce5b4e0ff2
101 files changed
+3364
-2018
No files matched your search
+50
-120
@@ -1,15 +1,18 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import router_dyndns.ddns_service as ddns_service
|
||||
from router_dyndns.ddns import DdnsSettings, DdnsStore, DnsProviderError, DnsReconciliationError, make_app
|
||||
import karldns.services.ddns as ddns_module
|
||||
from karldns import DdnsSettings, DdnsStore, DnsProviderError, DnsReconciliationError, make_app
|
||||
|
||||
|
||||
def test_update_accepts_fritzbox_placeholders(tmp_path: Path) -> None:
|
||||
def _update_path(account: dict[str, str]) -> str:
|
||||
return f"/api/v1/router/update/{account['update_slug']}"
|
||||
|
||||
|
||||
def test_router_update_get_accepts_fritzbox_placeholders(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.ddns.example.net", "router")
|
||||
@@ -17,121 +20,70 @@ def test_update_accepts_fritzbox_placeholders(tmp_path: Path) -> None:
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
"/nic/update",
|
||||
_update_path(account),
|
||||
params={
|
||||
"hostname": "home.ddns.example.net",
|
||||
"myip": "203.0.113.8",
|
||||
"myipv6": "2001:db8::8",
|
||||
"username": account["username"],
|
||||
"password": account["password"],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "good 203.0.113.8"
|
||||
records = client.get("/records", auth=("admin", "admin")).json()
|
||||
records = client.get("/api/v1/admin/records", auth=("admin", "admin")).json()
|
||||
assert records["total"] == 1
|
||||
assert records["items"][0]["hostname"] == "home.ddns.example.net"
|
||||
assert records["items"][0]["ipv4"] == "203.0.113.8"
|
||||
assert records["items"][0]["ipv6"] == "2001:db8::8"
|
||||
|
||||
|
||||
def test_update_rejects_wrong_password(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.ddns.example.net", "router")
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin", hostname_suffix="ddns.example.net"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
"/nic/update",
|
||||
params={"hostname": account["hostname"], "myip": "203.0.113.8", "username": account["username"], "password": "wrong"},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_update_rejects_hostname_outside_provider_suffix(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "router")
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin", hostname_suffix="ddns.example.net"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
"/nic/update",
|
||||
params={"hostname": account["hostname"], "myip": "203.0.113.8", "username": account["username"], "password": account["password"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_generated_account_can_update_own_hostname(tmp_path: Path) -> None:
|
||||
def test_router_update_post_accepts_form_and_json(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
"/nic/update",
|
||||
params={
|
||||
"hostname": "home.example.net",
|
||||
"myip": "203.0.113.9",
|
||||
"username": account["username"],
|
||||
"password": account["password"],
|
||||
},
|
||||
)
|
||||
form_response = client.post(_update_path(account), data={"myip": "203.0.113.9"})
|
||||
json_response = client.post(_update_path(account), json={"myip": "203.0.113.10"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "good 203.0.113.9"
|
||||
|
||||
slug_response = client.get(
|
||||
f"/u/{account['update_slug']}",
|
||||
params={"myip": "203.0.113.10"},
|
||||
)
|
||||
|
||||
assert slug_response.status_code == 200
|
||||
assert slug_response.text == "good 203.0.113.10"
|
||||
assert form_response.status_code == 200
|
||||
assert form_response.text == "good 203.0.113.9"
|
||||
assert json_response.status_code == 200
|
||||
assert json_response.text == "good 203.0.113.10"
|
||||
assert store.list_records()[0]["ipv4"] == "203.0.113.10"
|
||||
|
||||
|
||||
def test_one_box_update_url_can_authenticate_with_query_credentials(tmp_path: Path) -> None:
|
||||
def test_router_update_requires_registered_hostname(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
settings = DdnsSettings(
|
||||
database_path=database_path,
|
||||
admin_password="admin",
|
||||
public_base_url="http://ddns.example.net",
|
||||
)
|
||||
app = make_app(settings)
|
||||
account = store.create_account("home.example.net", "alice", accepted=False)
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
one_box_url = ddns_service.DdnsService(settings, store).credentialed_update_url(account)
|
||||
parsed = urlparse(one_box_url)
|
||||
params = parse_qs(parsed.query)
|
||||
|
||||
params["myip"] = ["203.0.113.11"]
|
||||
params["myipv6"] = [""]
|
||||
response = client.get(parsed.path, params={key: value[0] for key, value in params.items()})
|
||||
before = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
registered = client.get(f"/api/v1/router/register/{account['management_slug']}", params={"myip": "203.0.113.9"})
|
||||
after = client.get(_update_path(account), params={"myip": "203.0.113.10"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "good 203.0.113.11"
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert before.status_code == 404
|
||||
assert registered.status_code == 200
|
||||
assert registered.text == "good registered"
|
||||
assert after.status_code == 200
|
||||
assert after.text == "good 203.0.113.10"
|
||||
|
||||
|
||||
def test_dns_publish_failure_does_not_update_record(tmp_path: Path, monkeypatch, caplog) -> None:
|
||||
def fail_publish(result, settings):
|
||||
raise DnsProviderError("dns down")
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="router_dyndns.ddns_service")
|
||||
monkeypatch.setattr(ddns_service, "publish_dns", fail_publish)
|
||||
caplog.set_level(logging.WARNING, logger="karldns.services.ddns")
|
||||
monkeypatch.setattr(ddns_module, "publish_dns", fail_publish)
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
response = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
|
||||
assert response.status_code == 500
|
||||
assert store.list_records() == []
|
||||
@@ -146,15 +98,15 @@ def test_dns_reconciliation_failure_is_logged_separately(tmp_path: Path, monkeyp
|
||||
def fail_publish(result, settings):
|
||||
raise DnsReconciliationError("provider state mismatch")
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="router_dyndns.ddns_service")
|
||||
monkeypatch.setattr(ddns_service, "publish_dns", fail_publish)
|
||||
caplog.set_level(logging.WARNING, logger="karldns.services.ddns")
|
||||
monkeypatch.setattr(ddns_module, "publish_dns", fail_publish)
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
response = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
|
||||
assert response.status_code == 500
|
||||
assert store.list_records() == []
|
||||
@@ -167,7 +119,7 @@ def test_programming_error_during_publish_is_not_mapped_to_dns_failure(tmp_path:
|
||||
def fail_publish(result, settings):
|
||||
raise AssertionError("publish bug")
|
||||
|
||||
monkeypatch.setattr(ddns_service, "publish_dns", fail_publish)
|
||||
monkeypatch.setattr(ddns_module, "publish_dns", fail_publish)
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
@@ -175,7 +127,7 @@ def test_programming_error_during_publish_is_not_mapped_to_dns_failure(tmp_path:
|
||||
client = TestClient(app)
|
||||
|
||||
with pytest.raises(AssertionError, match="publish bug"):
|
||||
client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
|
||||
assert store.list_records() == []
|
||||
assert store.list_update_events() == []
|
||||
@@ -183,14 +135,14 @@ def test_programming_error_during_publish_is_not_mapped_to_dns_failure(tmp_path:
|
||||
|
||||
def test_dns_publish_success_updates_record(tmp_path: Path, monkeypatch) -> None:
|
||||
published = []
|
||||
monkeypatch.setattr(ddns_service, "publish_dns", lambda result, settings: published.append(result))
|
||||
monkeypatch.setattr(ddns_module, "publish_dns", lambda result, settings: published.append(result))
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
response = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "good 203.0.113.9"
|
||||
@@ -204,7 +156,7 @@ def test_dns_publish_success_updates_record(tmp_path: Path, monkeypatch) -> None
|
||||
|
||||
def test_unchanged_slug_update_is_read_only_for_history_and_record(tmp_path: Path, monkeypatch) -> None:
|
||||
published = []
|
||||
monkeypatch.setattr(ddns_service, "publish_dns", lambda result, settings: published.append(result))
|
||||
monkeypatch.setattr(ddns_module, "publish_dns", lambda result, settings: published.append(result))
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
@@ -213,8 +165,8 @@ def test_unchanged_slug_update_is_read_only_for_history_and_record(tmp_path: Pat
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
second_response = client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
response = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
second_response = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "nochg 203.0.113.9"
|
||||
@@ -233,7 +185,7 @@ def test_slug_update_rejects_hostname_outside_provider_suffix(tmp_path: Path) ->
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin", hostname_suffix="ddns.example.net"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
response = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
|
||||
assert response.status_code == 404
|
||||
assert store.list_records() == []
|
||||
@@ -241,7 +193,7 @@ def test_slug_update_rejects_hostname_outside_provider_suffix(tmp_path: Path) ->
|
||||
|
||||
def test_omitted_ip_family_is_preserved(tmp_path: Path, monkeypatch) -> None:
|
||||
published = []
|
||||
monkeypatch.setattr(ddns_service, "publish_dns", lambda result, settings: published.append(result))
|
||||
monkeypatch.setattr(ddns_module, "publish_dns", lambda result, settings: published.append(result))
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
@@ -249,7 +201,7 @@ def test_omitted_ip_family_is_preserved(tmp_path: Path, monkeypatch) -> None:
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"})
|
||||
response = client.get(_update_path(account), params={"myip": "203.0.113.9"})
|
||||
|
||||
assert response.status_code == 200
|
||||
record = store.list_records()[0]
|
||||
@@ -267,7 +219,7 @@ def test_blank_ip_params_fall_back_to_source_ip(tmp_path: Path) -> None:
|
||||
client = TestClient(app, client=("198.51.100.44", 50000))
|
||||
|
||||
response = client.get(
|
||||
f"/u/{account['update_slug']}",
|
||||
_update_path(account),
|
||||
params={"myip": "", "myipv6": ""},
|
||||
)
|
||||
|
||||
@@ -276,9 +228,9 @@ def test_blank_ip_params_fall_back_to_source_ip(tmp_path: Path) -> None:
|
||||
assert store.list_records()[0]["ipv4"] == "198.51.100.44"
|
||||
|
||||
|
||||
def test_invalid_legacy_ip_is_badip_and_does_not_change_record(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_invalid_ip_is_badip_and_does_not_change_record(tmp_path: Path, monkeypatch) -> None:
|
||||
published = []
|
||||
monkeypatch.setattr(ddns_service, "publish_dns", lambda result, settings: published.append(result))
|
||||
monkeypatch.setattr(ddns_module, "publish_dns", lambda result, settings: published.append(result))
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
@@ -286,7 +238,7 @@ def test_invalid_legacy_ip_is_badip_and_does_not_change_record(tmp_path: Path, m
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "not-an-ip"})
|
||||
response = client.get(_update_path(account), params={"myip": "not-an-ip"})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.text == "badip"
|
||||
@@ -296,7 +248,7 @@ def test_invalid_legacy_ip_is_badip_and_does_not_change_record(tmp_path: Path, m
|
||||
assert record["ipv6"] == "2001:db8::8"
|
||||
|
||||
|
||||
def test_wrong_family_legacy_ip_is_badip_and_preserves_records(tmp_path: Path) -> None:
|
||||
def test_wrong_ip_family_is_badip_and_preserves_records(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.example.net", "alice")
|
||||
@@ -304,32 +256,10 @@ def test_wrong_family_legacy_ip_is_badip_and_preserves_records(tmp_path: Path) -
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(f"/u/{account['update_slug']}", params={"myip": "2001:db8::9"})
|
||||
response = client.get(_update_path(account), params={"myip": "2001:db8::9"})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.text == "badip"
|
||||
record = store.list_records()[0]
|
||||
assert record["ipv4"] == "203.0.113.8"
|
||||
assert record["ipv6"] == "2001:db8::8"
|
||||
|
||||
|
||||
def test_nic_update_invalid_ip_returns_badip(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("home.ddns.example.net", "router")
|
||||
app = make_app(DdnsSettings(database_path=database_path, admin_password="admin", hostname_suffix="ddns.example.net"))
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
"/nic/update",
|
||||
params={
|
||||
"hostname": account["hostname"],
|
||||
"myipv6": "203.0.113.9",
|
||||
"username": account["username"],
|
||||
"password": account["password"],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.text == "badip"
|
||||
assert store.list_records() == []
|
||||
Reference in new issue
Block a user