Polish KarlDNS production UI and docs

This commit is contained in:
StasonJatham committed 2026-05-21 10:05:40 +02:00
1 parent ca92dc211a
commit 5beee0e5c6
61 files changed
+1566 -523

No files matched your search

+97 -33
View File
@@ -28,12 +28,12 @@ def test_self_service_managed_hostname_generates_random_account(tmp_path: Path)
assert response.status_code == 200
assert "Update-URL:" in response.text
assert "One-box update URL:" in response.text
assert "Full update URL:" in response.text
assert "CNAME target:" in response.text
assert "Benutzername (optional):" in response.text
assert "Kennwort (optional):" in response.text
assert "Copy complete FRITZ!Box fields" in response.text
assert "Copy CNAME record" in response.text
assert "Copy router fields" in response.text
assert "Copy CNAME" in response.text
assert "Manual updates" in response.text
assert "curl -sS" in response.text
assert "203.0.113.10" in response.text
@@ -61,15 +61,15 @@ def test_magic_hostname_without_login_generates_update_and_management_links(tmp_
assert response.status_code == 200
assert "Update-URL:" in response.text
assert "One-box update URL:" in response.text
assert "Full update URL:" in response.text
assert "CNAME target:" in response.text
assert "Benutzername (optional):" in response.text
assert "Kennwort (optional):" in response.text
assert "Private status page:" in response.text
assert "Copy complete FRITZ!Box fields" in response.text
assert "Copy CNAME record" in response.text
assert "FRITZ!Box replaces" in response.text
assert "Copy curl examples" in response.text
assert "Private dashboard:" in response.text
assert "Copy router fields" in response.text
assert "Copy CNAME" in response.text
assert "FRITZ!Box fills in" in response.text
assert "Copy curl" in response.text
assert response.text.count("Secret") >= 3
assert response.text.count("Public") >= 3
assert ".ddns.example.net" in response.text
@@ -94,12 +94,13 @@ def test_magic_management_link_can_delete_hostname(tmp_path: Path) -> None:
management = client.get(f"/m/{account['management_slug']}")
assert management.status_code == 200
assert '<link href="/admin.css?v=2" rel="stylesheet">' in management.text
assert "home.ddns.example.net" in management.text
assert "CNAME target:" in management.text
assert "Benutzername (optional):" in management.text
assert "Kennwort (optional):" in management.text
assert "Copy complete FRITZ!Box fields" in management.text
assert "Copy CNAME record" in management.text
assert "Copy router fields" in management.text
assert "Copy CNAME" in management.text
assert "Manual updates" in management.text
assert "curl -sS" in management.text
assert "Public:" in management.text
@@ -174,7 +175,7 @@ def test_management_dashboard_password_locks_dashboard_only(tmp_path: Path) -> N
data={"csrf": _csrf(locked.text), "dashboard_password": "wrong"},
)
assert bad_unlock.status_code == 401
assert "Incorrect dashboard password." in bad_unlock.text
assert "Incorrect password." in bad_unlock.text
assert "Update-URL:" not in bad_unlock.text
good_unlock = locked_client.post(
@@ -312,7 +313,7 @@ def test_existing_dashboard_password_requires_current_password_to_change_or_remo
assert change.status_code == 400
assert remove.status_code == 400
assert "Current dashboard password is incorrect." in change.text
assert "Current password is incorrect." in change.text
assert app.state.store.verify_dashboard_password("home.ddns.example.net", "correct horse")
@@ -331,21 +332,21 @@ def test_management_dashboard_rotates_update_url_and_router_credentials(tmp_path
dashboard = client.get(f"/m/{account['management_slug']}")
assert dashboard.status_code == 200
assert "Rotate Update-URL" in dashboard.text
assert "Rotate router credentials" in dashboard.text
assert "New update URL" in dashboard.text
assert "New password" in dashboard.text
assert "data-secret-toggle" in dashboard.text
assert 'type="password"' in dashboard.text
rotated_update = client.post(f"/m/{account['management_slug']}/rotate", data={"csrf": _csrf(dashboard.text), "action": "update"})
assert rotated_update.status_code == 200
assert "Update-URL rotated." in rotated_update.text
assert "Update URL changed. Update your router now." in rotated_update.text
assert "data-secret-input" in rotated_update.text
assert client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.9"}).status_code == 404
rotated_password = client.post(f"/m/{account['management_slug']}/rotate", data={"csrf": _csrf(rotated_update.text), "action": "password"})
assert rotated_password.status_code == 200
assert "Router credentials rotated" in rotated_password.text
assert "One-box update URL:" in rotated_password.text
assert "Router password changed" in rotated_password.text
assert "Full update URL:" in rotated_password.text
assert "Kennwort (optional):" in rotated_password.text
assert "data-secret-toggle" in rotated_password.text
assert client.get(
@@ -434,7 +435,7 @@ def test_management_dashboard_custom_domain_claim_flow(tmp_path: Path, monkeypat
dashboard = client.get(f"/m/{account['management_slug']}")
challenge = client.post(f"/m/{account['management_slug']}/domains/request", data={"csrf": _csrf(dashboard.text), "domain": "example.net"})
assert challenge.status_code == 200
assert "Domain claim created" in challenge.text
assert "Claim created" in challenge.text
assert "TXT name" in challenge.text
assert "TXT value" in challenge.text
assert "Claim secret" in challenge.text
@@ -449,17 +450,17 @@ def test_management_dashboard_custom_domain_claim_flow(tmp_path: Path, monkeypat
)
assert verified.status_code == 200
assert "Domain verified" in verified.text
assert "Create custom hostname" in verified.text
assert "Create hostname" in verified.text
created = client.post(
f"/m/{account['management_slug']}/domains/hostnames",
data={"csrf": _csrf(verified.text), "hostname": "home.example.net", "claim_secret": claim_secret},
)
assert created.status_code == 200
assert "Custom hostname created" in created.text
assert "Hostname created" in created.text
assert "home.example.net" in created.text
assert "Update-URL:" in created.text
assert "Private status page:" in created.text
assert "Private dashboard:" in created.text
def test_dashboard_domain_claims_are_bound_to_creating_dashboard(tmp_path: Path, monkeypatch) -> None:
@@ -557,7 +558,7 @@ def test_custom_domain_flow_works_without_login(tmp_path: Path, monkeypatch) ->
verify = client.post("/verify-domain", data={"domain": "example.net", "claim_secret": claim_secret})
assert verify.status_code == 200
assert "Create credentials" in verify.text
assert "Create hostname" in verify.text
created = client.post(
"/accounts",
@@ -584,7 +585,7 @@ def test_public_home_hides_admin_link_and_uses_automatic_router_login(tmp_path:
assert 'href="/admin"' not in response.text
assert "Router login name" not in response.text
assert 'name="username"' not in response.text
assert "generates the hostname, secret update URL, and optional router compatibility credentials automatically" in response.text
assert "You get a hostname, a private update URL, and router fields if your router needs them." in response.text
assert "Linux cron updater" in response.text
assert "curl -fsSL https://karldns.de/install.sh | sudo sh" in response.text
assert "curl -fsSL https://karldns.de/uninstall.sh | sudo sh" in response.text
@@ -617,27 +618,56 @@ def test_application_pages_use_stricter_script_csp(tmp_path: Path) -> None:
response = client.get("/")
assert response.status_code == 200
assert '<script src="/app.js" defer></script>' in response.text
assert '<link href="/app.css" rel="stylesheet">' in response.text
assert '<script src="/theme.js?v=2"></script>' in response.text
assert '<script src="/clipboard.js?v=2" defer></script>' in response.text
assert '<script src="/admin-tables.js?v=2" defer></script>' not in response.text
assert '<link href="/bootstrap-subset.css?v=3" rel="stylesheet">' in response.text
assert '<link href="/app.css?v=2" rel="stylesheet">' in response.text
assert '<link href="/admin.css" rel="stylesheet">' not in response.text
assert "cdn.jsdelivr.net/npm/bootstrap" not in response.text
assert "<style>" not in response.text
assert "script-src 'self'" in response.headers["content-security-policy"]
assert "script-src 'self' 'unsafe-inline'" not in response.headers["content-security-policy"]
assert "style-src 'self' https://cdn.jsdelivr.net" in response.headers["content-security-policy"]
assert "script-src 'self' 'unsafe-inline'" in response.headers["content-security-policy"]
assert "style-src 'self'" in response.headers["content-security-policy"]
assert "style-src 'self' 'unsafe-inline'" not in response.headers["content-security-policy"]
assert client.get("/app.js").status_code == 200
bootstrap_css = client.get("/bootstrap-subset.css")
assert bootstrap_css.status_code == 200
assert "text/css" in bootstrap_css.headers["content-type"]
assert bootstrap_css.headers["cache-control"] == "public, max-age=31536000, immutable"
assert client.get("/theme.js").status_code == 200
assert client.get("/clipboard.js").status_code == 200
assert client.get("/admin-tables.js").status_code == 200
app_css = client.get("/app.css")
assert app_css.status_code == 200
assert "text/css" in app_css.headers["content-type"]
assert app_css.headers["cache-control"] == "public, max-age=3600"
assert app_css.headers["cache-control"] == "public, max-age=31536000, immutable"
admin_css = client.get("/admin.css")
assert admin_css.status_code == 200
assert "text/css" in admin_css.headers["content-type"]
assert admin_css.headers["cache-control"] == "public, max-age=31536000, immutable"
def test_footer_links_and_privacy_page_are_available(tmp_path: Path) -> None:
app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin"))
app = make_app(
DdnsSettings(
database_path=tmp_path / "ddns.sqlite3",
admin_password="admin",
public_base_url="https://karldns.de",
)
)
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert '<link rel="canonical" href="https://karldns.de/">' in response.text
assert 'property="og:title" content="KarlDNS"' in response.text
assert 'property="og:url" content="https://karldns.de/"' in response.text
assert 'property="og:image" content="https://karldns.de/social-preview.png"' in response.text
assert 'property="og:image:width" content="1200"' in response.text
assert 'property="og:image:height" content="630"' in response.text
assert 'name="twitter:card" content="summary_large_image"' in response.text
assert 'application/ld+json' in response.text
assert "NetworkingApplication" in response.text
assert "Made by Karl" in response.text
assert 'href="https://github.com/StasonJatham/karldns.de"' in response.text
assert 'href="https://karl.fail"' in response.text
@@ -646,12 +676,41 @@ def test_footer_links_and_privacy_page_are_available(tmp_path: Path) -> None:
privacy = client.get("/privacy")
assert privacy.status_code == 200
assert '<link rel="canonical" href="https://karldns.de/privacy">' in privacy.text
assert "Privacy and legal information for KarlDNS" in privacy.text
assert "WebPage" in privacy.text
assert "Legal and privacy" in privacy.text
assert "Data processed" in privacy.text
assert "hashed router passwords" in privacy.text
assert "protected router passwords" in privacy.text
assert "Made by Karl" in privacy.text
def test_public_robots_and_sitemap_are_available(tmp_path: Path) -> None:
app = make_app(
DdnsSettings(
database_path=tmp_path / "ddns.sqlite3",
admin_password="admin",
admin_path="/ops",
public_base_url="https://karldns.de",
)
)
client = TestClient(app)
robots = client.get("/robots.txt")
sitemap = client.get("/sitemap.xml")
assert robots.status_code == 200
assert "User-agent: *" in robots.text
assert "Disallow: /ops/" in robots.text
assert "Disallow: /m/" in robots.text
assert "Disallow: /api/" in robots.text
assert "Sitemap: https://karldns.de/sitemap.xml" in robots.text
assert sitemap.status_code == 200
assert "application/xml" in sitemap.headers["content-type"]
assert "<loc>https://karldns.de/</loc>" in sitemap.text
assert "<loc>https://karldns.de/privacy</loc>" in sitemap.text
def test_web_icon_assets_and_manifest_are_exposed(tmp_path: Path) -> None:
app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin"))
client = TestClient(app)
@@ -666,11 +725,16 @@ def test_web_icon_assets_and_manifest_are_exposed(tmp_path: Path) -> None:
assert 'href="/site.webmanifest"' in response.text
for path in [
"/favicon.ico",
"/bootstrap-subset.css",
"/favicon-32x32.png",
"/favicon-16x16.png",
"/apple-touch-icon.png",
"/logo-nav.webp",
"/logo.webp",
"/logo-192.png",
"/logo-512.png",
"/social-preview.png",
"/social-preview.webp",
]:
asset = client.get(path)
assert asset.status_code == 200