tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit afaabd1b486f9266f3579e1403c08a5de488bc1f
parent 5ed4d80f2470e3d5693c5800a3ed6595d34a2f41
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 17:52:30 -0700

runtime: prove nip11 chorus parity

Diffstat:
Mcrates/tangle_runtime/src/nip11.rs | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 111 insertions(+), 2 deletions(-)

diff --git a/crates/tangle_runtime/src/nip11.rs b/crates/tangle_runtime/src/nip11.rs @@ -247,10 +247,10 @@ fn accepts_nostr_json(value: Option<&HeaderValue>) -> bool { #[cfg(test)] mod tests { - use super::{BaseRelayInfoConfig, base_relay_info_router}; + use super::{BaseRelayInfoConfig, base_relay_info_response, base_relay_info_router}; use crate::config::{BaseRelayRuntimeConfig, parse_base_relay_runtime_config_json}; use axum::body::to_bytes; - use http::{Request, StatusCode, header}; + use http::{HeaderMap, HeaderValue, Request, StatusCode, header}; use serde_json::{Value, json}; use tangle_crypto::RelaySigner; use tower::ServiceExt; @@ -299,6 +299,115 @@ mod tests { } #[tokio::test] + async fn nip11_preserves_chorus_relay_information_parity() { + let config = runtime_config(enabled_groups()); + let disabled_config = runtime_config(json!({"enabled": false})); + let document = BaseRelayInfoConfig::new("tangle", &config) + .expect("config") + .with_description("Tangle relay") + .with_contact("ops@radroots.test") + .with_icon("https://relay.radroots.test/icon.png") + .build_document() + .expect("document"); + let disabled = BaseRelayInfoConfig::new("tangle", &disabled_config) + .expect("disabled config") + .build_document() + .expect("disabled"); + let relay_self = RelaySigner::from_secret_hex(&"7".repeat(64)) + .expect("relay signer") + .public_key() + .clone(); + + assert_eq!(document.name, "tangle"); + assert_eq!(document.description.as_deref(), Some("Tangle relay")); + assert_eq!(document.contact.as_deref(), Some("ops@radroots.test")); + assert_eq!( + document.icon.as_deref(), + Some("https://relay.radroots.test/icon.png") + ); + assert_eq!(document.relay_self(), Some(relay_self.as_str())); + assert_eq!(document.supported_nips, vec![1, 11, 29, 42, 45, 70]); + for absent in [50, 77, 86, 98, 99] { + assert!(!document.supported_nips.contains(&absent)); + } + assert_eq!(disabled.supported_nips, vec![1, 11, 42, 45, 70]); + assert!(disabled.relay_self().is_none()); + assert_eq!(document.software, crate::TANGLE_RELAY_SOFTWARE); + assert_eq!(document.version, crate::TANGLE_RELAY_VERSION); + assert_eq!(document.limitation.max_message_length, 1_048_576); + assert_eq!(document.limitation.max_subscriptions, 64); + assert_eq!(document.limitation.max_filters, 10); + assert_eq!(document.limitation.max_limit, 500); + assert_eq!(document.limitation.max_query_complexity, 2_048); + assert_eq!(document.limitation.max_subid_length, 64); + assert_eq!(document.limitation.max_event_tags, 200); + assert_eq!(document.limitation.max_content_length, 65_536); + assert!(!document.limitation.auth_required); + assert!(!document.limitation.payment_required); + assert!(document.limitation.restricted_writes); + assert_eq!(document.limitation.default_limit, 100); + assert_eq!( + document.retention.accepted_events, + "accepted events are retained in canonical storage without a time-based expiration policy" + ); + assert_eq!( + document.retention.relay_generated_events, + "relay-generated group state events are retained with their source events" + ); + assert_eq!( + document.retention.group_visibility, + "private and hidden group policy gates visibility without implying physical deletion" + ); + assert!(!document.retention.physical_erasure); + assert!(!document.retention.compaction_guarantee); + + let mut headers = HeaderMap::new(); + headers.insert( + header::ACCEPT, + HeaderValue::from_static("application/nostr+json; q=1"), + ); + let response = base_relay_info_response(document.clone(), headers); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(header::CONTENT_TYPE).expect("type"), + "application/nostr+json" + ); + assert_eq!( + response + .headers() + .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) + .expect("origin"), + "*" + ); + assert_eq!( + response + .headers() + .get(header::ACCESS_CONTROL_ALLOW_HEADERS) + .expect("headers"), + "*" + ); + assert_eq!( + response + .headers() + .get(header::ACCESS_CONTROL_ALLOW_METHODS) + .expect("methods"), + "*" + ); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let value = serde_json::from_slice::<Value>(&body).expect("json"); + assert_eq!(value["software"], crate::TANGLE_RELAY_SOFTWARE); + assert_eq!(value["version"], crate::TANGLE_RELAY_VERSION); + assert_eq!(value["supported_nips"], json!([1, 11, 29, 42, 45, 70])); + assert_eq!(value["retention"]["physical_erasure"], false); + assert_eq!(value["retention"]["compaction_guarantee"], false); + + let rejected = base_relay_info_response(document, HeaderMap::new()); + assert_eq!(rejected.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] async fn nip11_router_serves_nostr_json_only_for_nostr_accept() { let config = runtime_config(enabled_groups()); let document = BaseRelayInfoConfig::new("tangle", &config)