From 67d8dca48224748f5dcc3d4497d395093d6b640d Mon Sep 17 00:00:00 2001 From: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Date: Tue, 25 Aug 2026 14:56:17 -0400 Subject: [PATCH] test(projects): prove scoped discovery bound Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> --- crates/buzz-cli/src/commands/projects.rs | 57 +++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/crates/buzz-cli/src/commands/projects.rs b/crates/buzz-cli/src/commands/projects.rs index bee8cf8029..817dc95c57 100644 --- a/crates/buzz-cli/src/commands/projects.rs +++ b/crates/buzz-cli/src/commands/projects.rs @@ -78,13 +78,21 @@ pub(crate) const PROJECT_QUERY_EVENT_BOUND: u32 = 10_000; pub(crate) async fn fetch_projects_for_channel( client: &BuzzClient, channel: &str, +) -> Result, CliError> { + fetch_projects_for_channel_bounded(client, channel, PROJECT_QUERY_EVENT_BOUND).await +} + +async fn fetch_projects_for_channel_bounded( + client: &BuzzClient, + channel: &str, + max_events: u32, ) -> Result, CliError> { let filter = serde_json::json!({ "kinds": [KIND_PROJECT], "#buzz-channel": [channel], }); let events: Vec = client - .query_all_bounded(filter, PROJECT_QUERY_EVENT_BOUND) + .query_all_bounded(filter, max_events) .await? .into_iter() .map(|event| { @@ -970,6 +978,53 @@ mod tests { assert_eq!(body[0]["kinds"], serde_json::json!([KIND_PROJECT])); } + #[tokio::test] + async fn channel_scoping_prevents_unrelated_heads_from_consuming_the_bound() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let channel = "11111111-1111-4111-8111-111111111111"; + let target = build_project("target", None, None, &[], Some(channel), None) + .unwrap() + .sign_with_keys(&nostr::Keys::generate()) + .unwrap(); + let decoy_channel = "22222222-2222-4222-8222-222222222222"; + let decoys = ["decoy-a", "decoy-b"].map(|slug| { + build_project(slug, None, None, &[], Some(decoy_channel), None) + .unwrap() + .sign_with_keys(&nostr::Keys::generate()) + .unwrap() + }); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let base_url = format!("http://{}", listener.local_addr().unwrap()); + let server = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.unwrap(); + let mut buf = vec![0; 65_536]; + let read = socket.read(&mut buf).await.unwrap(); + let request = String::from_utf8_lossy(&buf[..read]); + let body = request.split("\r\n\r\n").nth(1).unwrap(); + let filter: serde_json::Value = serde_json::from_str(body).unwrap(); + let response_body = if filter[0]["#buzz-channel"] == serde_json::json!([channel]) { + serde_json::to_string(&[target]).unwrap() + } else { + serde_json::to_string(&decoys).unwrap() + }; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{response_body}", + response_body.len() + ); + socket.write_all(response.as_bytes()).await.unwrap(); + }); + let client = + crate::client::BuzzClient::new(base_url, nostr::Keys::generate(), None, None).unwrap(); + + let projects = fetch_projects_for_channel_bounded(&client, channel, 1) + .await + .unwrap(); + server.await.unwrap(); + assert_eq!(projects.len(), 1); + assert_eq!(project_slug(&projects[0]).as_deref(), Some("target")); + } + #[test] fn project_channel_matching_ignores_unrelated_claims() { let expected = "11111111-1111-4111-8111-111111111111";