Archived
Simplify DDNS flow and polish UI
This commit is contained in:
1 parent
5beee0e5c6
commit
5a90bd9d87
42 files changed
+587
-1418
No files matched your search
+55
-17
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import sqlite3
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
@@ -57,7 +58,7 @@ def test_cleanup_deletes_old_rate_limit_minute_buckets(tmp_path: Path) -> None:
|
||||
("recent", current_window - 30, 1),
|
||||
)
|
||||
|
||||
store.cleanup(challenge_hours=1, unused_account_hours=1)
|
||||
store.cleanup(unused_account_hours=1)
|
||||
|
||||
with store.connect() as conn:
|
||||
keys = {row[0] for row in conn.execute("SELECT key FROM rate_limits")}
|
||||
@@ -100,7 +101,6 @@ def test_first_run_creates_schema_and_repeated_startup_is_idempotent(tmp_path: P
|
||||
assert {
|
||||
"accounts",
|
||||
"cleanup_runs",
|
||||
"domain_challenges",
|
||||
"rate_limits",
|
||||
"records",
|
||||
"scheduled_tasks",
|
||||
@@ -111,6 +111,56 @@ def test_first_run_creates_schema_and_repeated_startup_is_idempotent(tmp_path: P
|
||||
assert store.list_scheduled_tasks()[0]["name"] == "cleanup"
|
||||
|
||||
|
||||
def test_startup_migrates_legacy_cleanup_run_columns(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
with sqlite3.connect(database_path) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE cleanup_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TEXT NOT NULL,
|
||||
domain_challenges_deleted INTEGER NOT NULL,
|
||||
abandoned_setups_deleted INTEGER NOT NULL,
|
||||
unused_accounts_deleted INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO cleanup_runs(
|
||||
created_at,
|
||||
domain_challenges_deleted,
|
||||
abandoned_setups_deleted,
|
||||
unused_accounts_deleted
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
("2026-01-01T00:00:00+00:00", 3, 2, 1),
|
||||
)
|
||||
|
||||
store = DdnsStore(database_path)
|
||||
|
||||
with store.connect() as conn:
|
||||
columns = {row[1] for row in conn.execute("PRAGMA table_info(cleanup_runs)")}
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT unused_accounts_deleted
|
||||
FROM cleanup_runs
|
||||
"""
|
||||
).fetchone()
|
||||
assert columns == {"id", "created_at", "unused_accounts_deleted"}
|
||||
assert tuple(row) == (1,)
|
||||
|
||||
|
||||
def test_cleanup_records_only_unused_hostname_count(tmp_path: Path) -> None:
|
||||
store = DdnsStore(tmp_path / "ddns.sqlite3")
|
||||
|
||||
result = store.cleanup(unused_account_hours=1)
|
||||
|
||||
assert result["unused_accounts"] == 0
|
||||
assert set(store.list_cleanup_runs()[0]) == {"created_at", "unused_accounts_deleted"}
|
||||
|
||||
|
||||
def test_sqlite_startup_tuning_is_idempotent(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
|
||||
@@ -147,8 +197,6 @@ def test_sqlite_startup_tuning_is_idempotent(tmp_path: Path) -> None:
|
||||
assert user_version == 1
|
||||
assert {
|
||||
"idx_accounts_created_at",
|
||||
"idx_domain_challenges_claim_hash",
|
||||
"idx_domain_challenges_unverified_created",
|
||||
"idx_rate_limits_window",
|
||||
"idx_update_events_hostname_created_at",
|
||||
"idx_update_event_rollups_hostname_last_seen_at",
|
||||
@@ -193,20 +241,19 @@ def test_cleanup_prunes_old_raw_update_events(tmp_path: Path) -> None:
|
||||
("2000-01-01T00:00:00+00:00", "old.example.net"),
|
||||
)
|
||||
|
||||
result = store.cleanup(challenge_hours=1, unused_account_hours=1, event_history_hours=24)
|
||||
result = store.cleanup(unused_account_hours=1, event_history_hours=24)
|
||||
|
||||
assert result["update_events"] == 1
|
||||
events = store.list_update_events()
|
||||
assert [event["hostname"] for event in events] == ["new.example.net"]
|
||||
|
||||
|
||||
def test_background_cleanup_removes_abandoned_setup_flows(tmp_path: Path) -> None:
|
||||
def test_background_cleanup_removes_abandoned_hostnames(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
app = make_app(
|
||||
DdnsSettings(
|
||||
database_path=database_path,
|
||||
admin_password="admin",
|
||||
cleanup_challenge_hours=1,
|
||||
cleanup_unused_account_hours=1,
|
||||
cleanup_interval_seconds=1,
|
||||
)
|
||||
@@ -214,26 +261,17 @@ def test_background_cleanup_removes_abandoned_setup_flows(tmp_path: Path) -> Non
|
||||
store = DdnsStore(database_path)
|
||||
|
||||
with TestClient(app):
|
||||
abandoned_claim = store.create_domain_challenge("abandoned.example.net")
|
||||
verified_claim = store.create_domain_challenge("verified.example.net")
|
||||
abandoned_account = store.create_account("abandoned.ddns.example.net", "router")
|
||||
active_account = store.create_account("active.ddns.example.net", "router")
|
||||
store.upsert("active.ddns.example.net", "203.0.113.10", None)
|
||||
|
||||
old = "2000-01-01T00:00:00+00:00"
|
||||
with store.connect() as conn:
|
||||
conn.execute("UPDATE domain_challenges SET created_at = ? WHERE domain = ?", (old, abandoned_claim["domain"]))
|
||||
conn.execute(
|
||||
"UPDATE domain_challenges SET created_at = ?, verified_at = ? WHERE domain = ?",
|
||||
(old, old, verified_claim["domain"]),
|
||||
)
|
||||
conn.execute("UPDATE accounts SET created_at = ? WHERE hostname = ?", (old, abandoned_account["hostname"]))
|
||||
conn.execute("UPDATE accounts SET created_at = ? WHERE hostname = ?", (old, active_account["hostname"]))
|
||||
|
||||
time.sleep(1.3)
|
||||
|
||||
assert store.get_domain_challenge("abandoned.example.net") is None
|
||||
assert store.get_domain_challenge("verified.example.net") is not None
|
||||
assert store.get_account_by_slug(abandoned_account["update_slug"]) is None
|
||||
assert store.get_account_by_slug(active_account["update_slug"]) is not None
|
||||
scheduled_task = store.list_scheduled_tasks()[0]
|
||||
@@ -248,7 +286,7 @@ def test_scheduled_cleanup_failure_is_logged(tmp_path: Path, monkeypatch, caplog
|
||||
store = DdnsStore(database_path)
|
||||
store.ensure_scheduled_task(app_module.CLEANUP_TASK_NAME, settings.cleanup_interval_seconds)
|
||||
|
||||
def fail_cleanup(challenge_hours, unused_account_hours, event_history_hours):
|
||||
def fail_cleanup(unused_account_hours, event_history_hours):
|
||||
raise RuntimeError("cleanup exploded")
|
||||
|
||||
caplog.set_level(logging.ERROR, logger="router_dyndns.app")
|
||||
|
||||
Reference in new issue
Block a user