Zo doen wijdat in oost

This commit is contained in:
2026-10-16 07:20:44 +02:00
parent 25d9cb1c7d
commit 6f80a820da
229 changed files with 23894 additions and 74 deletions
+1
View File
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
+247
View File
@@ -0,0 +1,247 @@
# retoor <retoor@molodetz.nl>
import time
import pytest
from fastapi.testclient import TestClient
from devplacepy.database import get_setting, get_table, set_setting
from devplacepy.main import app
from devplacepy.routers.admin import communities as router_module
from devplacepy.services.community.runner import CommunityRunner
from devplacepy.services.community.store import store
from devplacepy.utils import register_account
from tests.unit.services.community.fakes import FakeFetcher, json_community, no_sleep
_counter = [0]
def unique(prefix: str) -> str:
_counter[0] += 1
return f"{prefix}{int(time.time() * 1000)}{_counter[0]}"
def make_account(role: str = "Member") -> dict:
name = unique("cmty")
uid, _, _ = register_account(name, f"{name}@t.dev", "secret123", age_band="adult", accepted_terms=True)
if role != "Member":
get_table("users").update({"uid": uid, "role": role}, ["uid"])
row = get_table("users").find_one(uid=uid)
return {"username": name, "headers": {"X-API-KEY": row["api_key"], "Accept": "application/json"}}
@pytest.fixture(scope="module")
def client():
previous = {key: get_setting(key, "") for key in ("sync_enabled", "sync_origins")}
set_setting("sync_enabled", "1")
set_setting("sync_origins", "")
with TestClient(app) as test_client:
yield test_client
for key, value in previous.items():
set_setting(key, value)
@pytest.fixture
def fake_world(monkeypatch):
routes = json_community()
monkeypatch.setattr(
router_module,
"CommunityRunner",
lambda: CommunityRunner(fetcher_factory=lambda: FakeFetcher(routes), sleep=no_sleep),
)
get_table("communities").delete()
get_table("community_runs").delete()
get_table("sync_links").delete(origin="forum.test")
return routes
@pytest.fixture
def admin(client):
return make_account("Admin")
def wait_for(predicate, seconds: float = 10.0):
deadline = time.time() + seconds
while time.time() < deadline:
value = predicate()
if value:
return value
time.sleep(0.1)
raise AssertionError("condition never became true")
def add_ready(client, admin) -> dict:
response = client.post("/admin/communities", data={"query": "https://forum.test/api/posts"}, headers=admin["headers"])
assert response.status_code == 200, response.text
return wait_for(lambda: (row := store.get("forum-test")) and row["status"] == "ready" and row)
def test_member_is_refused(client, admin, fake_world):
member = make_account()
assert client.get("/admin/communities", headers=member["headers"], follow_redirects=False).status_code in (303, 403)
assert client.post("/admin/communities", data={"query": "x.test"}, headers=member["headers"], follow_redirects=False).status_code in (303, 403)
def test_add_learns_and_shows_detail(client, admin, fake_world):
row = add_ready(client, admin)
assert row["origin"] == "forum.test"
assert row["schema"]["feeds"][0]["type"] == "json_api"
assert row["description"] == "A friendly forum about code."
assert row["user_count"] == 1234
listing = client.get("/admin/communities", headers=admin["headers"])
assert listing.status_code == 200
assert listing.json()["items"][0]["slug"] == "forum-test"
detail = client.get("/admin/communities/forum-test", headers=admin["headers"])
assert detail.status_code == 200
body = detail.json()
assert body["feeds"][0]["pagination"] == "page"
assert body["collectors_lines"] > 100
html = client.get("/admin/communities/forum-test", headers={"X-API-KEY": admin["headers"]["X-API-KEY"]})
assert html.status_code == 200 and "Learned feeds" in html.text
def test_add_is_idempotent(client, admin, fake_world):
add_ready(client, admin)
again = client.post("/admin/communities", data={"query": "https://forum.test/api/posts"}, headers=admin["headers"])
assert again.status_code == 200
assert len(store.list()) == 1
def test_dry_run_writes_nothing_and_keeps_watermark(client, admin, fake_world):
row = add_ready(client, admin)
users_before = get_table("users").count()
response = client.post("/admin/communities/forum-test/run", data={"dry_run": "true"}, headers=admin["headers"])
assert response.status_code == 200
run = wait_for(lambda: next((item for item in store.runs(row["uid"]) if item["status"] == "dry_run"), None))
assert run["dry_run"] == 1
assert run["pushed"]["posts"]["created"] == 10
assert "DRY RUN" in run["log"]
assert get_table("users").count() == users_before
after = store.get("forum-test")
assert not after["watermark"]
assert after["run_count"] == 0
def test_live_run_creates_then_is_unchanged(client, admin, fake_world):
row = add_ready(client, admin)
client.post("/admin/communities/forum-test/run", data={}, headers=admin["headers"])
first = wait_for(lambda: next((item for item in store.runs(row["uid"]) if item["status"] in ("ok", "partial", "failed")), None))
assert first["status"] == "ok", first["errors"]
assert first["pushed"]["posts"]["created"] == 10
assert get_table("sync_links").count(origin="forum.test", kind="posts") == 10
done = store.get("forum-test")
assert done["run_count"] == 1 and done["watermark"]["feeds"]["feed1"]["since"]
assert done["next_run_at"]
client.post("/admin/communities/forum-test/run", data={}, headers=admin["headers"])
second = wait_for(lambda: len([item for item in store.runs(row["uid"]) if item["status"] != "running"]) >= 2 and store.runs(row["uid"])[0])
assert second["pushed"].get("posts", {}).get("created", 0) == 0
def test_live_run_refused_when_sync_disabled(client, admin, fake_world):
row = add_ready(client, admin)
set_setting("sync_enabled", "0")
try:
client.post("/admin/communities/forum-test/run", data={}, headers=admin["headers"])
run = wait_for(lambda: next((item for item in store.runs(row["uid"]) if item["status"] != "running"), None))
assert run["status"] == "failed"
assert any("disabled" in error for error in run["errors"])
finally:
set_setting("sync_enabled", "1")
def test_update_and_delete(client, admin, fake_world):
add_ready(client, admin)
client.post("/admin/communities/forum-test/update", data={"interval_seconds": "7200", "enabled": "on"}, headers=admin["headers"])
assert store.get("forum-test")["interval_seconds"] == 7200
bad = client.post("/admin/communities/forum-test/update", data={"interval_seconds": "10"}, headers=admin["headers"])
assert bad.status_code in (400, 422)
client.post("/admin/communities/forum-test/delete", headers=admin["headers"])
assert store.get("forum-test") is None
assert get_table("communities").find_one(slug="forum-test")["deleted_at"]
def test_unknown_community_is_404(client, admin, fake_world):
assert client.get("/admin/communities/nope", headers=admin["headers"]).status_code == 404
GATED = [
("get", "/admin/communities", {}),
("post", "/admin/communities", {"query": "https://forum.test/api/posts"}),
("get", "/admin/communities/forum-test", {}),
("post", "/admin/communities/forum-test/run", {}),
("post", "/admin/communities/forum-test/relearn", {}),
("post", "/admin/communities/forum-test/update", {"interval_seconds": "7200"}),
("post", "/admin/communities/forum-test/delete", {}),
]
@pytest.mark.parametrize("method,path,data", GATED)
def test_every_endpoint_refuses_guests_and_members_and_changes_nothing(client, admin, fake_world, method, path, data):
add_ready(client, admin)
before = store.get("forum-test")
member = make_account()
for headers in ({"Accept": "application/json"}, member["headers"]):
response = client.request(method.upper(), path, data=data or None, headers=headers, follow_redirects=False)
assert response.status_code in (303, 401, 403), (headers, response.status_code)
after = store.get("forum-test")
assert after is not None
assert after["interval_seconds"] == before["interval_seconds"] and len(store.list()) == 1
def test_password_is_stored_but_never_returned(client, admin, fake_world):
client.post(
"/admin/communities",
data={"query": "https://forum.test/api/posts", "login_username": "bot", "login_password": "s3cret-pw"},
headers=admin["headers"],
)
row = wait_for(lambda: (found := store.get("forum-test")) and found["status"] == "ready" and found)
assert row["login_password"] == "s3cret-pw"
for url in ("/admin/communities", "/admin/communities/forum-test"):
body = client.get(url, headers=admin["headers"])
assert "s3cret-pw" not in body.text and body.json()
page = client.get("/admin/communities/forum-test", headers={"X-API-KEY": admin["headers"]["X-API-KEY"]})
assert "s3cret-pw" not in page.text and "(stored)" in page.text
client.post("/admin/communities/forum-test/update", data={"interval_seconds": "7200", "login_username": "other"}, headers=admin["headers"])
assert store.get("forum-test")["login_password"] == "s3cret-pw"
client.post("/admin/communities/forum-test/update", data={"interval_seconds": "7200", "clear_login": "on"}, headers=admin["headers"])
assert store.get("forum-test")["login_password"] == ""
def test_devii_tools_are_admin_only_and_delete_needs_confirmation():
from devplacepy.services.devii.actions.catalog.community import COMMUNITY_ACTIONS
from devplacepy.services.devii.actions.dispatcher import CONFIRM_REQUIRED
from devplacepy.docs_api.groups import community
assert {action.name for action in COMMUNITY_ACTIONS} >= {"community_add", "community_run", "community_delete"}
assert all(action.requires_admin for action in COMMUNITY_ACTIONS)
assert "community_delete" in CONFIRM_REQUIRED
assert next(action for action in COMMUNITY_ACTIONS if action.name == "community_delete").params[-1].name == "confirm"
assert community.GROUP["admin"] is True
def test_update_saves_sync_options_and_keeps_unsent_ones(client, admin, fake_world):
add_ready(client, admin)
body = {"interval_seconds": 86400, "enabled": "true", "alias": "true", "sample_percent": 40, "topics": "devlog, rant"}
assert client.post("/admin/communities/forum-test/update", data=body, headers=admin["headers"]).status_code == 200
options = store.get("forum-test")["sync_options"]
assert options["alias"] is True and options["sample_percent"] == 40 and options["topics"] == ["devlog", "rant"]
client.post("/admin/communities/forum-test/update", data={"interval_seconds": 86400, "jitter_percent": 10}, headers=admin["headers"])
kept = store.get("forum-test")["sync_options"]
assert kept["alias"] is True and kept["jitter_percent"] == 10
def test_update_rejects_bad_options_and_late_aliasing(client, admin, fake_world):
add_ready(client, admin)
bad = client.post("/admin/communities/forum-test/update", data={"interval_seconds": 86400, "topics": "nonsense"}, headers=admin["headers"])
assert bad.status_code == 400
get_table("sync_links").insert({"uid": "late1", "origin": "forum.test", "kind": "users", "origin_uid": "1", "local_uid": "x", "fingerprint": "f", "origin_updated_at": "", "first_synced_at": "", "synced_at": ""})
late = client.post("/admin/communities/forum-test/update", data={"interval_seconds": 86400, "alias": "true"}, headers=admin["headers"])
assert late.status_code == 400
assert not store.get("forum-test")["sync_options"].get("alias")
def test_sync_options_are_admin_only(client, fake_world):
member = make_account("Member")
add_ready(client, make_account("Admin"))
assert client.get("/admin/communities/forum-test", headers=member["headers"]).status_code in (401, 403)
assert client.post("/admin/communities/forum-test/update", data={"interval_seconds": 86400, "alias": "true"}, headers=member["headers"]).status_code in (401, 403)
+1
View File
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
+36
View File
@@ -0,0 +1,36 @@
# retoor <retoor@molodetz.nl>
import pytest
from fastapi.testclient import TestClient
from devplacepy.database import get_setting, set_setting
from devplacepy.database.nodes import create_device
from devplacepy.main import app
from tests.api.nodes.conftest import make_member
JSON = {"Accept": "application/json"}
def admin_headers(who: dict) -> dict:
return {**who["headers"], **JSON}
def make_admin() -> dict:
return make_member(consent=False, enabled=False, role="Admin")
def make_device(owner_uid: str, label: str = "Home laptop") -> dict:
return create_device(owner_uid, label=label, kind="native")[0]
@pytest.fixture(scope="module")
def client():
previous = get_setting("nodes_enabled", "0")
with TestClient(app) as test_client:
yield test_client
set_setting("nodes_enabled", previous)
@pytest.fixture
def admin():
return make_admin()
+34
View File
@@ -0,0 +1,34 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database.nodes import list_flags, raise_flag
from tests.api.admin.nodes.conftest import admin_headers, make_admin, make_device
from tests.api.nodes.conftest import make_member
def test_member_cannot_resolve(client):
member = make_member()
assert client.post("/admin/nodes/flags/x/resolve", headers=admin_headers(member)).status_code == 403
def test_resolve_closes_the_flag_once(client, admin):
owner = make_member()
device = make_device(owner["uid"])
flag = raise_flag("refusal_mismatch", device_uid=device["uid"], user_uid=owner["uid"], detail="test")
listed = client.get("/admin/nodes", headers=admin_headers(admin)).json()["flags"]
assert flag["uid"] in {item["uid"] for item in listed}
assert client.post(f"/admin/nodes/flags/{flag['uid']}/resolve", headers=admin_headers(admin)).status_code == 200
assert flag["uid"] not in {item["uid"] for item in list_flags()}
assert client.post(f"/admin/nodes/flags/{flag['uid']}/resolve", headers=admin_headers(admin)).status_code == 409
def test_unknown_flag_is_404(client, admin):
assert client.post("/admin/nodes/flags/nope/resolve", headers=admin_headers(admin)).status_code == 404
def test_junior_admin_cannot_resolve_a_senior_admins_flag(client):
senior = make_admin()
junior = make_admin()
device = make_device(senior["uid"])
flag = raise_flag("integrity_failure", device_uid=device["uid"], user_uid=senior["uid"], detail="test")
client.post(f"/admin/nodes/flags/{flag['uid']}/resolve", headers=admin_headers(junior))
assert flag["uid"] in {item["uid"] for item in list_flags()}
+42
View File
@@ -0,0 +1,42 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database.nodes import LIMITS
from tests.api.admin.nodes.conftest import admin_headers, make_device
from tests.api.nodes.conftest import make_member
def test_guest_is_refused(client):
assert client.get("/admin/nodes", headers={"Accept": "application/json"}).status_code == 401
def test_member_is_refused(client):
member = make_member()
assert client.get("/admin/nodes", headers=admin_headers(member)).status_code == 403
def test_admin_overview_shape(client, admin):
owner = make_member()
device = make_device(owner["uid"], "Overview laptop")
body = client.get("/admin/nodes", headers=admin_headers(admin)).json()
assert set(body["pool"]) >= {"online", "connected", "capacity", "open_circuits"}
assert body["queue"]["max_depth"] == LIMITS.max_queue_depth
assert body["limits"]["max_connected_nodes"] == LIMITS.max_connected_nodes
assert {item["key"] for item in body["settings"]} == {
"nodes_max_total",
"nodes_daily_quota_user",
"nodes_daily_quota_guest",
"nodes_cache_ttl_seconds",
"nodes_daily_social_user",
"nodes_social_via_nodes",
}
listed = {row["uid"]: row for row in body["devices"]}
assert listed[device["uid"]]["owner_username"] == owner["username"]
assert len(body["devices"]) <= 100
assert "secret_hash" not in listed[device["uid"]]
def test_html_page_renders(client, admin):
response = client.get("/admin/nodes", headers=admin["headers"])
assert response.status_code == 200
assert "Community nodes" in response.text
assert "data-live=\"summary\"" in response.text
+24
View File
@@ -0,0 +1,24 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_setting, set_setting
from tests.api.admin.nodes.conftest import admin_headers
from tests.api.nodes.conftest import make_member
def test_member_cannot_flip_the_switch(client):
member = make_member()
assert client.post("/admin/nodes/killswitch", json={"enabled": True}, headers=admin_headers(member)).status_code == 403
def test_admin_sets_and_flips_the_switch(client, admin):
previous = get_setting("nodes_enabled", "0")
try:
set_setting("nodes_enabled", "0")
on = client.post("/admin/nodes/killswitch", json={"enabled": True}, headers=admin_headers(admin))
assert on.json()["data"] == {"enabled": True}
assert get_setting("nodes_enabled", "0") == "1"
flipped = client.post("/admin/nodes/killswitch", json={}, headers=admin_headers(admin))
assert flipped.json()["data"] == {"enabled": False}
assert get_setting("nodes_enabled", "1") == "0"
finally:
set_setting("nodes_enabled", previous)
+36
View File
@@ -0,0 +1,36 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_setting, set_setting
from devplacepy.database.nodes import LIMITS
from tests.api.admin.nodes.conftest import admin_headers
from tests.api.nodes.conftest import make_member
KEYS = ("nodes_max_total", "nodes_daily_quota_user", "nodes_social_via_nodes")
def test_member_cannot_change_settings(client):
member = make_member()
assert client.post("/admin/nodes/settings", json={"nodes_max_total": 1}, headers=admin_headers(member)).status_code == 403
def test_only_submitted_fields_change_and_ceilings_clamp(client, admin):
previous = {key: get_setting(key, "") for key in KEYS}
try:
response = client.post(
"/admin/nodes/settings",
json={"nodes_max_total": LIMITS.max_connected_nodes + 500, "nodes_social_via_nodes": "1"},
headers=admin_headers(admin),
)
data = response.json()["data"]
assert "nodes_max_total" in data["clamped"]
assert get_setting("nodes_max_total", "") == str(LIMITS.max_connected_nodes)
assert get_setting("nodes_social_via_nodes", "") == "1"
assert get_setting("nodes_daily_quota_user", "") == previous["nodes_daily_quota_user"]
finally:
for key, value in previous.items():
set_setting(key, value)
def test_negative_and_unknown_values_are_rejected(client, admin):
assert client.post("/admin/nodes/settings", json={"nodes_max_total": -1}, headers=admin_headers(admin)).status_code in (400, 422)
assert client.post("/admin/nodes/settings", json={"nodes_social_via_nodes": "2"}, headers=admin_headers(admin)).status_code in (400, 422)
+47
View File
@@ -0,0 +1,47 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database.nodes import get_device
from tests.api.admin.nodes.conftest import admin_headers, make_admin, make_device
from tests.api.nodes.conftest import make_member
def test_member_cannot_suspend(client):
member = make_member()
device = make_device(member["uid"])
response = client.post(f"/admin/nodes/devices/{device['uid']}/suspend", json={"minutes": 5}, headers=admin_headers(member))
assert response.status_code == 403
def test_suspend_and_unsuspend_round_trip(client, admin):
owner = make_member()
device = make_device(owner["uid"])
url = f"/admin/nodes/devices/{device['uid']}"
response = client.post(f"{url}/suspend", json={"minutes": 30, "reason": "noisy"}, headers=admin_headers(admin))
assert response.json()["data"] == {"uid": device["uid"], "minutes": 30}
row = get_device(device["uid"])
assert row["status"] == "suspended"
assert row["suspend_reason"] == "noisy"
assert client.post(f"{url}/unsuspend", headers=admin_headers(admin)).status_code == 200
assert get_device(device["uid"])["status"] == "active"
assert client.post(f"{url}/unsuspend", headers=admin_headers(admin)).status_code == 409
def test_minutes_are_bounded(client, admin):
owner = make_member()
device = make_device(owner["uid"])
url = f"/admin/nodes/devices/{device['uid']}/suspend"
for minutes in (0, 10081):
assert client.post(url, json={"minutes": minutes}, headers=admin_headers(admin)).status_code in (400, 422)
def test_unknown_device_is_404(client, admin):
response = client.post("/admin/nodes/devices/nope/suspend", json={"minutes": 5}, headers=admin_headers(admin))
assert response.status_code == 404
def test_junior_admin_cannot_suspend_a_senior_admins_device(client):
senior = make_admin()
junior = make_admin()
device = make_device(senior["uid"])
client.post(f"/admin/nodes/devices/{device['uid']}/suspend", json={"minutes": 5}, headers=admin_headers(junior))
assert get_device(device["uid"])["status"] == "active"