tangle


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

commit b8a35d94462be39b2a4fac96fec573437f89b14f
parent 59fc9a26b5e91ebca6aaf17179cf2cef1004d278
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 08:50:57 -0700

tests: cover websocket group lifecycle

Diffstat:
Mcrates/tangle_runtime/tests/phase2_acceptance_targets.rs | 222++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 221 insertions(+), 1 deletion(-)

diff --git a/crates/tangle_runtime/tests/phase2_acceptance_targets.rs b/crates/tangle_runtime/tests/phase2_acceptance_targets.rs @@ -30,7 +30,8 @@ use tangle_runtime::{ }; use tangle_test_support::{ FixtureKey, TANGLE_V2_RELAY_SECRET_HEX, TANGLE_V2_RELAY_URL, tangle_v2_auth_event, - tangle_v2_event, tangle_v2_group_create_event, + tangle_v2_event, tangle_v2_group_create_event, tangle_v2_group_event, + tangle_v2_group_metadata_event, tangle_v2_join_event, tangle_v2_put_user_event, }; use tokio::{net::TcpListener, time::timeout}; use tokio_tungstenite::tungstenite::{Message as TungsteniteMessage, client::IntoClientRequest}; @@ -446,6 +447,194 @@ async fn websocket_public_relay_covers_query_count_ephemeral_and_rejection_flows } #[tokio::test] +async fn websocket_nip29_group_lifecycle_state_and_live_paths_are_integrated() { + let root = temp_root("acceptance-nip29-websocket"); + let _ = std::fs::remove_dir_all(&root); + let listener = TcpListener::bind("127.0.0.1:0").await.expect("listener"); + let address = listener.local_addr().expect("address"); + let runtime = TangleRuntime::open(runtime_config(&root, address)).expect("runtime"); + let shutdown = runtime.shutdown_signal().clone(); + let task = tokio::spawn(serve_listener_until_shutdown(runtime, listener)); + let mut owner = connect_nostr_socket(address).await; + let mut member = connect_nostr_socket(address).await; + let mut outsider = connect_nostr_socket(address).await; + let mut observer = connect_nostr_socket(address).await; + let owner_challenge = read_auth_challenge(&mut owner).await; + let member_challenge = read_auth_challenge(&mut member).await; + let outsider_challenge = read_auth_challenge(&mut outsider).await; + let _ = read_auth_challenge(&mut observer).await; + let auth_created_at = current_unix_timestamp(); + + authenticate_client( + &mut owner, + FixtureKey::Owner, + &owner_challenge, + auth_created_at, + ) + .await; + authenticate_client( + &mut member, + FixtureKey::Member, + &member_challenge, + auth_created_at.saturating_add(1), + ) + .await; + authenticate_client( + &mut outsider, + FixtureKey::Outsider, + &outsider_challenge, + auth_created_at.saturating_add(2), + ) + .await; + + let create = tangle_v2_group_create_event(FixtureKey::Owner, "SocketFarm", 1_714_124_440, &[]) + .expect("create"); + send_client_value(&mut owner, json!(["EVENT", event_to_value(&create)])).await; + assert_ok(read_relay_value(&mut owner).await, &create, true, ""); + + let denied_join = + tangle_v2_join_event(FixtureKey::Outsider, "SocketFarm", 1_714_124_441).expect("join"); + send_client_value( + &mut outsider, + json!(["EVENT", event_to_value(&denied_join)]), + ) + .await; + assert_ok( + read_relay_value(&mut outsider).await, + &denied_join, + false, + "restricted: group is unavailable", + ); + + let metadata = tangle_v2_group_metadata_event( + FixtureKey::Owner, + "SocketFarm", + "Socket Market", + 1_714_124_442, + &[], + ) + .expect("metadata"); + send_client_value(&mut owner, json!(["EVENT", event_to_value(&metadata)])).await; + assert_ok(read_relay_value(&mut owner).await, &metadata, true, ""); + + let put_member = tangle_v2_put_user_event( + FixtureKey::Owner, + "SocketFarm", + FixtureKey::Member, + 1_714_124_443, + ) + .expect("put member"); + send_client_value(&mut owner, json!(["EVENT", event_to_value(&put_member)])).await; + assert_ok(read_relay_value(&mut owner).await, &put_member, true, ""); + + for (subscription_id, kind) in [ + ("metadata-count", KIND_GROUP_METADATA), + ("admins-count", KIND_GROUP_ADMINS), + ("members-count", KIND_GROUP_MEMBERS), + ] { + send_client_value( + &mut observer, + json!(["COUNT", subscription_id, {"kinds":[kind], "#d":["SocketFarm"]}]), + ) + .await; + assert_eq!( + read_relay_value(&mut observer).await, + json!(["COUNT", subscription_id, {"count": 1}]) + ); + } + + for (subscription_id, kind) in [ + ("metadata-state", KIND_GROUP_METADATA), + ("admins-state", KIND_GROUP_ADMINS), + ("members-state", KIND_GROUP_MEMBERS), + ] { + send_client_value( + &mut observer, + json!(["REQ", subscription_id, {"kinds":[kind], "#d":["SocketFarm"]}]), + ) + .await; + assert_relay_event_kind_tag( + read_relay_value(&mut observer).await, + subscription_id, + kind, + "d", + "SocketFarm", + ); + assert_eq!( + read_relay_value(&mut observer).await, + json!(["EOSE", subscription_id]) + ); + send_client_value(&mut observer, json!(["CLOSE", subscription_id])).await; + expect_no_relay_message(&mut observer).await; + } + + send_client_value( + &mut observer, + json!(["REQ", "group-live", {"kinds":[1], "#h":["SocketFarm"]}]), + ) + .await; + assert_eq!( + read_relay_value(&mut observer).await, + json!(["EOSE", "group-live"]) + ); + + let group_note = tangle_v2_group_event( + FixtureKey::Member, + "SocketFarm", + 1_714_124_444, + 1, + "harvest", + ) + .expect("group note"); + send_client_value(&mut member, json!(["EVENT", event_to_value(&group_note)])).await; + assert_ok(read_relay_value(&mut member).await, &group_note, true, ""); + assert_live_event( + read_relay_value(&mut observer).await, + "group-live", + &group_note, + ); + + send_client_value( + &mut observer, + json!(["COUNT", "group-note-count", {"kinds":[1], "#h":["SocketFarm"]}]), + ) + .await; + assert_eq!( + read_relay_value(&mut observer).await, + json!(["COUNT", "group-note-count", {"count": 1}]) + ); + + send_client_value( + &mut observer, + json!(["REQ", "group-note-query", {"kinds":[1], "#h":["SocketFarm"]}]), + ) + .await; + assert_live_event( + read_relay_value(&mut observer).await, + "group-note-query", + &group_note, + ); + assert_eq!( + read_relay_value(&mut observer).await, + json!(["EOSE", "group-note-query"]) + ); + + shutdown.request_shutdown(); + read_websocket_close(&mut owner).await; + read_websocket_close(&mut member).await; + read_websocket_close(&mut outsider).await; + read_websocket_close(&mut observer).await; + let report = timeout(Duration::from_secs(2), task) + .await + .expect("shutdown timeout") + .expect("task") + .expect("serve"); + assert_eq!(report.listen_addr(), address); + + let _ = std::fs::remove_dir_all(root); +} + +#[tokio::test] async fn nip11_includes_cors_headers_and_truthful_supported_nips() { let root = temp_root("acceptance-nip11"); let _ = std::fs::remove_dir_all(&root); @@ -1056,6 +1245,17 @@ async fn expect_no_relay_message(socket: &mut TestWebSocket) { ); } +async fn authenticate_client( + socket: &mut TestWebSocket, + fixture_key: FixtureKey, + challenge: &str, + created_at: u64, +) { + let auth = tangle_v2_auth_event(fixture_key, challenge, created_at).expect("auth"); + send_client_value(socket, json!(["AUTH", event_to_value(&auth)])).await; + assert_ok(read_relay_value(socket).await, &auth, true, ""); +} + fn assert_notice_prefix(value: Value, prefix: &str) { assert_eq!(value[0], "NOTICE"); assert!(value[1].as_str().expect("notice").starts_with(prefix)); @@ -1071,6 +1271,26 @@ fn assert_live_event(value: Value, subscription_id: &str, event: &Event) { assert_eq!(value[2]["id"], event.id().as_str()); } +fn assert_relay_event_kind_tag( + value: Value, + subscription_id: &str, + kind: u32, + tag_name: &str, + tag_value: &str, +) { + assert_eq!(value[0], "EVENT"); + assert_eq!(value[1], subscription_id); + assert_eq!(value[2]["kind"], json!(kind)); + let tags = value[2]["tags"].as_array().expect("tags"); + assert!(tags.iter().any(|tag| { + let Some(parts) = tag.as_array() else { + return false; + }; + parts.first().and_then(Value::as_str) == Some(tag_name) + && parts.get(1).and_then(Value::as_str) == Some(tag_value) + })); +} + fn phase2_projection_with_group( group_id: &str, metadata: GroupMetadata,