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
+108
-8
@@ -3,27 +3,29 @@ import logging
|
||||
import sqlite3
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import router_dyndns.app as app_module
|
||||
from router_dyndns.ddns import DdnsSettings, DdnsStore, make_app
|
||||
import karldns.app as app_module
|
||||
from karldns import DdnsSettings, DdnsStore, make_app
|
||||
from karldns.security import hash_lookup_token
|
||||
|
||||
|
||||
def test_rate_limit_reject_does_not_increment_bucket(tmp_path: Path) -> None:
|
||||
store = DdnsStore(tmp_path / "ddns.sqlite3")
|
||||
|
||||
assert store.allow_rate_limit("198.51.100.1:/magic", 1) is True
|
||||
assert store.allow_rate_limit("198.51.100.1:/magic", 1) is False
|
||||
assert store.allow_rate_limit("198.51.100.1:/", 1) is True
|
||||
assert store.allow_rate_limit("198.51.100.1:/", 1) is False
|
||||
store.flush_rate_limits()
|
||||
|
||||
window = int(time.time()) // 60
|
||||
with store.connect() as conn:
|
||||
count = conn.execute(
|
||||
"SELECT count FROM rate_limits WHERE key = ? AND window = ?",
|
||||
("198.51.100.1:/magic", window),
|
||||
("198.51.100.1:/", window),
|
||||
).fetchone()[0]
|
||||
assert count == 1
|
||||
|
||||
@@ -161,6 +163,52 @@ def test_cleanup_records_only_unused_hostname_count(tmp_path: Path) -> None:
|
||||
assert set(store.list_cleanup_runs()[0]) == {"created_at", "unused_accounts_deleted"}
|
||||
|
||||
|
||||
def test_cleanup_removes_pending_generated_setups_after_cutoff(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("pending.ddns.example.net", "router", accepted=False)
|
||||
|
||||
with store.connect() as conn:
|
||||
conn.execute(
|
||||
"UPDATE accounts SET created_at = ? WHERE hostname = ?",
|
||||
("2000-01-01T00:00:00+00:00", account["hostname"]),
|
||||
)
|
||||
|
||||
result = store.cleanup(unused_account_hours=45 * 24)
|
||||
|
||||
assert result["unused_accounts"] == 1
|
||||
assert store.get_account_by_slug(account["update_slug"]) is None
|
||||
|
||||
|
||||
def test_cleanup_keeps_recent_pending_setup_until_pending_cutoff(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("pending.ddns.example.net", "router", accepted=False)
|
||||
|
||||
recent = (datetime.now(UTC) - timedelta(hours=2)).isoformat()
|
||||
with store.connect() as conn:
|
||||
conn.execute(
|
||||
"UPDATE accounts SET created_at = ? WHERE hostname = ?",
|
||||
(recent, account["hostname"]),
|
||||
)
|
||||
|
||||
result = store.cleanup(unused_account_hours=1, pending_account_hours=24)
|
||||
|
||||
assert result["unused_accounts"] == 0
|
||||
assert store.get_account_by_slug(account["update_slug"]) is not None
|
||||
|
||||
|
||||
def test_pending_generated_setup_stays_pending_after_restart(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
store = DdnsStore(database_path)
|
||||
account = store.create_account("pending.ddns.example.net", "router", accepted=False)
|
||||
store.close()
|
||||
|
||||
restarted = DdnsStore(database_path)
|
||||
|
||||
assert restarted.get_account_by_slug(account["update_slug"])["accepted_at"] is None
|
||||
|
||||
|
||||
def test_sqlite_startup_tuning_is_idempotent(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
|
||||
@@ -194,7 +242,7 @@ def test_sqlite_startup_tuning_is_idempotent(tmp_path: Path) -> None:
|
||||
assert cache_size == -20_000
|
||||
assert temp_store == 2
|
||||
assert mmap_size == 256 * 1024 * 1024
|
||||
assert user_version == 1
|
||||
assert user_version == 3
|
||||
assert {
|
||||
"idx_accounts_created_at",
|
||||
"idx_rate_limits_window",
|
||||
@@ -203,6 +251,58 @@ def test_sqlite_startup_tuning_is_idempotent(tmp_path: Path) -> None:
|
||||
}.issubset(index_names)
|
||||
|
||||
|
||||
def test_accounts_schema_migration_removes_legacy_router_credentials(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "ddns.sqlite3"
|
||||
with sqlite3.connect(database_path) as conn:
|
||||
conn.execute("PRAGMA user_version = 2")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE accounts (
|
||||
hostname TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
update_slug TEXT NOT NULL UNIQUE,
|
||||
management_hash TEXT UNIQUE,
|
||||
dashboard_password_hash TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
accepted_at TEXT,
|
||||
disabled INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO accounts(
|
||||
hostname, username, token_hash, update_slug, management_hash,
|
||||
dashboard_password_hash, created_at, accepted_at, disabled
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, 0)
|
||||
""",
|
||||
(
|
||||
"home.ddns.example.net",
|
||||
"router",
|
||||
"old-token-hash",
|
||||
"update-secret",
|
||||
hash_lookup_token("dashboard-secret"),
|
||||
"2026-05-21T00:00:00+00:00",
|
||||
"2026-05-21T00:00:00+00:00",
|
||||
),
|
||||
)
|
||||
|
||||
store = DdnsStore(database_path)
|
||||
account = store.get_account_by_slug("update-secret")
|
||||
|
||||
with store.connect() as conn:
|
||||
columns = {row[1] for row in conn.execute("PRAGMA table_info(accounts)")}
|
||||
user_version = conn.execute("PRAGMA user_version").fetchone()[0]
|
||||
|
||||
assert user_version == 3
|
||||
assert "username" not in columns
|
||||
assert "token_hash" not in columns
|
||||
assert account
|
||||
assert account["hostname"] == "home.ddns.example.net"
|
||||
|
||||
|
||||
def test_rate_limit_flush_requeues_counts_after_database_failure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
store = DdnsStore(tmp_path / "ddns.sqlite3")
|
||||
assert store.allow_rate_limit("198.51.100.1:/api", 60) is True
|
||||
@@ -286,10 +386,10 @@ 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(unused_account_hours, event_history_hours):
|
||||
def fail_cleanup(unused_account_hours, event_history_hours, pending_account_hours):
|
||||
raise RuntimeError("cleanup exploded")
|
||||
|
||||
caplog.set_level(logging.ERROR, logger="router_dyndns.app")
|
||||
caplog.set_level(logging.ERROR, logger="karldns.app")
|
||||
monkeypatch.setattr(store, "cleanup", fail_cleanup)
|
||||
|
||||
asyncio.run(app_module._run_cleanup_if_due(store, settings))
|
||||
|
||||
Reference in new issue
Block a user