Simplify DDNS flow and polish UI

This commit is contained in:
StasonJatham committed 2026-05-21 14:18:39 +02:00
1 parent 5beee0e5c6
commit 5a90bd9d87
42 files changed
+587 -1418

No files matched your search

-102
View File
@@ -63,11 +63,8 @@ def test_api_docs_include_versioned_ddns_api(tmp_path: Path) -> None:
assert "/events" not in schema["paths"]
assert "/" not in schema["paths"]
assert "/magic" not in schema["paths"]
assert "/request-domain" not in schema["paths"]
assert "/verify-domain" not in schema["paths"]
assert "/accounts" not in schema["paths"]
assert "/m/{management_slug}" not in schema["paths"]
assert "/d/{claim_secret}" not in schema["paths"]
assert any(tag["name"] == "hostnames" for tag in schema["tags"])
assert all(tag["name"] != "admin" for tag in schema["tags"])
management_tag = next(tag for tag in schema["tags"] if tag["name"] == "management")
@@ -113,9 +110,6 @@ def test_public_redoc_only_exposes_api_and_router_endpoints(tmp_path: Path) -> N
schema = client.get("/openapi.json").json()
expected_public_paths = {
"/api/v1/domains/challenges",
"/api/v1/domains/verify",
"/api/v1/hostnames/custom",
"/api/v1/hostnames/magic",
"/api/v1/management/{management_slug}",
"/api/v1/updates/{update_slug}",
@@ -144,16 +138,8 @@ def test_openapi_marks_generated_update_links_as_secret(tmp_path: Path) -> None:
("path", "payload"),
[
("/api/v1/hostnames/magic", {"username": "router", "unexpected": True}),
("/api/v1/domains/challenges", {"domain": "example.net", "unexpected": True}),
("/api/v1/domains/verify", {"domain": "example.net", "claim_secret": "secret", "unexpected": True}),
(
"/api/v1/hostnames/custom",
{"hostname": "home.example.net", "claim_secret": "secret", "unexpected": True},
),
],
)
def test_public_write_models_reject_extra_json_fields(tmp_path: Path, path: str, payload: dict[str, object]) -> None:
app = make_app(
DdnsSettings(
@@ -235,50 +221,6 @@ def test_api_dns_publish_failure_keeps_record_unchanged(tmp_path: Path, monkeypa
assert store.list_update_events()[0]["status"] == "dns_failed"
def test_api_custom_hostname_requires_verified_domain(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(ddns_service, "dns_txt_contains", lambda name, expected: True)
app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin"))
client = TestClient(app)
challenge = client.post("/api/v1/domains/challenges", json={"domain": "example.net"})
assert challenge.status_code == 201
challenge_body = challenge.json()
verified = client.post(
"/api/v1/domains/verify",
json={"domain": "example.net", "claim_secret": challenge_body["claim_secret"]},
)
assert verified.status_code == 200
assert verified.json()["verified"] is True
hostname = client.post(
"/api/v1/hostnames/custom",
json={
"hostname": "home.example.net",
"claim_secret": challenge_body["claim_secret"],
"username": "router",
},
)
assert hostname.status_code == 201
assert hostname.json()["hostname"] == "home.example.net"
def test_domain_challenge_verification_requires_current_secret_and_owner(tmp_path: Path) -> None:
store = DdnsStore(tmp_path / "ddns.sqlite3")
first = store.create_domain_challenge("example.net", owner_hostname="owner.example.net")
second = store.create_domain_challenge("example.net", owner_hostname="owner.example.net")
assert store.verify_domain_challenge("example.net", first["claim_secret"], owner_hostname="owner.example.net") is None
assert store.verify_domain_challenge("example.net", second["claim_secret"], owner_hostname="other.example.net") is None
assert store.get_domain_challenge("example.net", second["claim_secret"], owner_hostname="owner.example.net")["verified_at"] is None
verified = store.verify_domain_challenge("example.net", second["claim_secret"], owner_hostname="owner.example.net")
assert verified is not None
assert verified["verified_at"]
def test_api_creation_is_rate_limited(tmp_path: Path) -> None:
app = make_app(
DdnsSettings(
@@ -309,47 +251,3 @@ def test_api_invalid_ip_returns_clear_400(tmp_path: Path) -> None:
assert response.status_code == 400
assert response.json()["detail"] == "myipv6 must be an IPv6 address"
assert store.list_records() == []
def test_verified_domain_challenge_cannot_be_overwritten(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(ddns_service, "dns_txt_contains", lambda name, expected: True)
app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin"))
client = TestClient(app)
challenge = client.post("/api/v1/domains/challenges", json={"domain": "example.net"}).json()
verified = client.post(
"/api/v1/domains/verify",
json={"domain": "example.net", "claim_secret": challenge["claim_secret"]},
)
overwrite = client.post("/api/v1/domains/challenges", json={"domain": "example.net"})
assert verified.status_code == 200
assert overwrite.status_code == 409
def test_custom_hostname_must_be_inside_publishable_dns_zone(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(ddns_service, "dns_txt_contains", lambda name, expected: True)
app = make_app(
DdnsSettings(
database_path=tmp_path / "ddns.sqlite3",
admin_password="admin",
dns_provider="cloudflare",
cloudflare_api_token="token",
cloudflare_zone_id="zone",
dns_zones={"ddns.example.net"},
)
)
client = TestClient(app)
challenge = client.post("/api/v1/domains/challenges", json={"domain": "example.net"}).json()
client.post(
"/api/v1/domains/verify",
json={"domain": "example.net", "claim_secret": challenge["claim_secret"]},
)
hostname = client.post(
"/api/v1/hostnames/custom",
json={"hostname": "home.example.net", "claim_secret": challenge["claim_secret"]},
)
assert hostname.status_code == 403
assert hostname.json()["detail"] == "hostname is outside configured DNS publishing zones"