Files
smirk-backend-core/tests/landing.rs
T
jwinterm 51a3371be1 feat(admin,landing): admin-plane hardening + public landing [operator §2]
Admin posture (§2.1/§2.2):
- Hard fail-closed bind guard in main: a non-loopback ADMIN_BIND refuses startup
  unless ADMIN_ALLOW_PUBLIC_BIND=true (was a warning).
- admin_plane_guard middleware on the admin router: Host allowlist
  (localhost/127.0.0.1/[::1]/configured TOR_ADMIN_ONION; a missing Host is allowed
  since rebinding must supply one and the bind is the primary control) to defeat
  DNS-rebinding against the loopback socket, plus X-Frame-Options: DENY +
  CSP frame-ancestors 'none' (anti-clickjacking). Still no CORS on the admin plane.

Public landing (§2.3), default-OFF, one fingerprint surface:
- GET /api/v1/server-info — built from a hand-written ServerInfo allowlist (never
  by serializing config/state). Default projection is minimal { software.name,
  status }; version (coarse major.minor), chains, price_feed, and uptime (bool
  `up` only) are each individually opt-in. The enabled-feature tuple is NEVER
  emitted anonymously (connected clients use /capabilities).
- GET / — minimal HTML (operator title HTML-escaped); bare 404 when off (does not
  hint an admin plane exists). server-info also 404s when off.
- config: ADMIN_ALLOW_PUBLIC_BIND, TOR_ADMIN_ONION; PUBLIC_LANDING_TITLE +
  per-field PUBLIC_EXPOSE_VERSION/CHAINS/PRICE_FEED toggles.

Tests: host_allowlist + html_escape unit tests; landing off->on minimal-projection
(asserts only software+status, no version/chains/features); admin-plane Host-403 +
anti-clickjacking headers. Landing/admin stay off the public OpenAPI.

Deferred (documented): §2.5 NIP-05 consent-gating (changes existing
/.well-known/nostr.json behavior — needs the standing v0.2.4-compat verification
first), §2.3 k-anon public stats, response-header/TLS-fingerprint stripping (proxy
/ops-doc concern).
2026-06-27 15:32:11 -04:00

47 lines
1.8 KiB
Rust

//! L1 integration: the public landing surface (operator §2.3). Default-OFF; when
//! on, the default projection is the minimal `{ software.name, status }` and `/`
//! serves HTML. One sequential test (it toggles process env).
mod common;
use axum::http::StatusCode;
#[tokio::test]
async fn landing_off_then_on_minimal() {
// Off (default): both the read model and `/` are 404 (no admin-plane hint).
std::env::remove_var("PUBLIC_LANDING_ENABLED");
let Some(app) = common::try_app().await else {
return;
};
let (st, _) = app.request("GET", "/api/v1/server-info", None, None).await;
assert_eq!(st, StatusCode::NOT_FOUND);
let (st, _) = app.request("GET", "/", None, None).await;
assert_eq!(st, StatusCode::NOT_FOUND);
// On, default toggles: minimal projection — only software.name + status.
std::env::set_var("PUBLIC_LANDING_ENABLED", "true");
let app = common::try_app().await.expect("app");
let (st, body) = app.request("GET", "/api/v1/server-info", None, None).await;
assert_eq!(st, StatusCode::OK);
let obj = body.as_object().expect("object");
assert_eq!(obj.len(), 2, "only software + status by default: {body}");
assert_eq!(body["software"]["name"], "smirk-backend-core");
assert_eq!(body["status"], "ok");
assert!(
body["software"]
.as_object()
.unwrap()
.get("version")
.is_none(),
"version omitted by default"
);
for hidden in ["chains", "price_feed", "up", "features"] {
assert!(obj.get(hidden).is_none(), "{hidden} omitted by default");
}
let (st, _) = app.request("GET", "/", None, None).await;
assert_eq!(st, StatusCode::OK, "landing HTML served when enabled");
std::env::remove_var("PUBLIC_LANDING_ENABLED");
}