mod common; use std::fs; use common::{ask, client, collection, config_for, publish, spawn_relay, test_envelope}; use frxd::index::LocalIndex; use frxd::message::{EXPOSURE_FULL, TYPE_RESPONSE}; use frxd::node::Node; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn two_node_broadcast_query_flow() { let root = tempfile::tempdir().unwrap(); let relay_url = spawn_relay().await; let shared_dir = root.path().join("bob/shared"); let private_dir = root.path().join("bob/private"); fs::create_dir_all(&shared_dir).unwrap(); fs::create_dir_all(&private_dir).unwrap(); fs::write( shared_dir.join("alpha.txt"), "Tantivy provides BM25 relevance scoring for rust search", ) .unwrap(); fs::write( shared_dir.join("beta.txt"), "Rust lifetimes and borrowing explained", ) .unwrap(); fs::write(private_dir.join("gamma.txt"), "rust secret launch codes").unwrap(); let bob_config = config_for(&root.path().join("bob"), "bob", &relay_url); { let index = LocalIndex::open(&bob_config.index_dir()).unwrap(); index .add_collection(&collection("shared", &shared_dir, true, EXPOSURE_FULL)) .unwrap(); index .add_collection(&collection("private", &private_dir, false, "metadata")) .unwrap(); } let _bob = Node::start(bob_config).await.unwrap(); let alice_dir = root.path().join("alice"); let alice_local = root.path().join("alice_mine"); fs::create_dir_all(&alice_local).unwrap(); fs::write(alice_local.join("notes.txt"), "my rust notes").unwrap(); let alice_config = config_for(&alice_dir, "alice", &relay_url); { let index = LocalIndex::open(&alice_config.index_dir()).unwrap(); index .add_collection(&collection("mine", &alice_local, false, "metadata")) .unwrap(); } let alice = Node::start(alice_config).await.unwrap(); let http = client(); let addr = alice.addr.to_string(); let (status, raw, value) = ask(&http, &addr, "rust", 1).await; assert!(status.is_success()); assert_eq!( value.pointer("/local/total").and_then(|v| v.as_u64()), Some(1) ); let responses = value.get("responses").and_then(|v| v.as_array()).unwrap(); assert_eq!(responses.len(), 1, "expected one responder: {value}"); let response = &responses[0]; let results = response.get("results").and_then(|v| v.as_array()).unwrap(); assert_eq!(results.len(), 1); assert_eq!( response.get("truncated").and_then(|v| v.as_bool()), Some(true) ); assert_eq!( response.get("more_available").and_then(|v| v.as_u64()), Some(1) ); assert!(!raw.contains("gamma"), "unshared collection leaked: {raw}"); assert!(!raw.contains("\"score\""), "response carries scores: {raw}"); let (_status, _raw, value) = ask(&http, &addr, "rust", 5).await; let responses = value.get("responses").and_then(|v| v.as_array()).unwrap(); assert_eq!(responses.len(), 1); assert_eq!( responses[0] .get("results") .and_then(|v| v.as_array()) .unwrap() .len(), 2 ); assert_eq!( responses[0].get("truncated").and_then(|v| v.as_bool()), Some(false) ); let (_status, _raw, value) = ask(&http, &addr, "secret launch codes", 5).await; let responses = value.get("responses").and_then(|v| v.as_array()).unwrap(); assert!( responses.is_empty(), "private collection was served: {value}" ); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn relay_rejects_non_query_broadcasts() { let relay_url = spawn_relay().await; let key = frxd::crypto::Keypair::generate(); let envelope = test_envelope( &key, TYPE_RESPONSE, serde_json::json!({"qid": "x", "results": []}), ); let response = publish(&client(), &relay_url, &envelope).await; assert_eq!(response.status(), reqwest::StatusCode::BAD_REQUEST); }