- audit() writes are serialized: pg_advisory_xact_lock on Postgres; in-process RLock held until session commit/rollback on SQLite (test: 4 concurrent writers x 25 entries -> chain verifies, 100 rows) - legacy rows (entry_hash NULL, pre-0002) are skipped: the chain begins GENESIS at the first hashed row; migrated DBs verify clean by design - monitor anchor: publishes the last chain hash as a signed NIP-01 note once per day, logs it in the audit log (self-verified before publish) tests: concurrent writers; legacy-genesis verification; anchor signs, logs once per day.
109 lines
3.4 KiB
Python
109 lines
3.4 KiB
Python
"""monitor tests — page renders, no sensitive data, escapes input."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from cayceops.components.monitor import monitor
|
|
from cayceops.models import ContentItem, DaemonRun, MailMessage, Proposal
|
|
|
|
|
|
def _seed(sess):
|
|
sess.add(DaemonRun(daemon="scanner", status="ok", detail={"proposed": 3}))
|
|
sess.add(Proposal(kind="affiliate", title="Title with <script>alert(1)</script>"))
|
|
sess.add(
|
|
MailMessage(
|
|
direction="in",
|
|
from_addr="secret-person@private.example.org",
|
|
subject="private subject",
|
|
classification="approval",
|
|
draft_status="drafted",
|
|
)
|
|
)
|
|
sess.add(ContentItem(theme="atm-fees", channel="nostr", caption="caption", status="draft"))
|
|
sess.flush()
|
|
|
|
|
|
def test_build_renders_all_sections(cfg, sess, tmp_path):
|
|
_seed(sess)
|
|
out = tmp_path / "status.html"
|
|
monitor._build(cfg(CAYCE_OPS_MONITOR_OUTPUT=str(out)), sess)
|
|
page = out.read_text()
|
|
|
|
assert "daemon health" in page
|
|
assert "proposal queue" in page
|
|
assert "recent audit activity" in page
|
|
assert "scanner" in page
|
|
assert "proposed: <strong>1</strong>" in page
|
|
assert "mail drafts awaiting approval: <strong>1</strong>" in page
|
|
|
|
|
|
def test_build_leaks_no_sensitive_data(cfg, sess, tmp_path):
|
|
_seed(sess)
|
|
out = tmp_path / "status.html"
|
|
monitor._build(cfg(CAYCE_OPS_MONITOR_OUTPUT=str(out)), sess)
|
|
page = out.read_text()
|
|
|
|
assert "secret-person@private.example.org" not in page # no addresses
|
|
assert "private subject" not in page # no subjects
|
|
assert "caption" not in page # no content bodies
|
|
assert "Title with" not in page # proposal titles not rendered
|
|
|
|
|
|
def test_build_escapes_injected_content(cfg, sess, tmp_path):
|
|
from datetime import UTC, datetime
|
|
|
|
from cayceops.models import AuditEntry
|
|
|
|
sess.add(
|
|
AuditEntry(
|
|
ts=datetime.now(UTC),
|
|
actor="<script>x</script>",
|
|
action="probe",
|
|
detail={},
|
|
)
|
|
)
|
|
sess.flush()
|
|
out = tmp_path / "status.html"
|
|
monitor._build(cfg(CAYCE_OPS_MONITOR_OUTPUT=str(out)), sess)
|
|
page = out.read_text()
|
|
assert "<script>x</script>" not in page
|
|
assert "<script>x</script>" in page
|
|
|
|
|
|
def test_anchor_signs_and_logs_once_per_day(cfg, sess, monkeypatch):
|
|
from cayceops.components.monitor import monitor
|
|
from cayceops.models import AuditEntry
|
|
|
|
# seed the chain
|
|
from cayceops.models import audit as audit_fn
|
|
|
|
audit_fn(sess, actor="daemon:executor", action="execute_done", detail={})
|
|
sess.flush()
|
|
|
|
published = []
|
|
|
|
def fake_publish(relays, event, timeout=10.0):
|
|
published.append(event)
|
|
return [{"relay": r, "ok": "true"} for r in relays]
|
|
|
|
import cayceops.components.publisher.nostr as nostr_mod
|
|
|
|
monkeypatch.setattr(nostr_mod, "publish", fake_publish)
|
|
|
|
config = cfg(
|
|
CAYCE_OPS_NOSTR_PRIVATE_KEY="11" * 32,
|
|
CAYCE_OPS_NOSTR_RELAYS="wss://r.example.org",
|
|
CAYCE_OPS_DEPLOYMENT_ID="itest",
|
|
)
|
|
code = monitor._anchor(config, sess)
|
|
assert code == 0
|
|
assert len(published) == 1
|
|
content = published[0]["content"]
|
|
assert "audit anchor" in content and "itest" in content
|
|
entry = sess.query(AuditEntry).filter_by(action="anchor").one()
|
|
assert entry.detail["last_hash"]
|
|
|
|
# second call same day -> skipped, no new nostr event
|
|
code2 = monitor._anchor(config, sess)
|
|
assert code2 == 0
|
|
assert len(published) == 1
|