mod common; use std::collections::BTreeSet; use std::fs; use common::{client, collection, config_for, poll, publish, spawn_relay, test_envelope, unicast}; use frxd::crypto::Keypair; use frxd::index::LocalIndex; use frxd::message::{ AggregateBody, QueryBody, ResponseItem, TYPE_AGGREGATE, TYPE_QUERY, TYPE_RESPONSE, build_response, }; use frxd::node::{self, Node}; use serde_json::Value; fn assert_exact_keys(value: &Value, expected: &[&str]) { let actual: BTreeSet<&str> = value .as_object() .expect("object") .keys() .map(String::as_str) .collect(); let expected: BTreeSet<&str> = expected.iter().copied().collect(); assert_eq!(actual, expected, "fields drifted: {value}"); } fn assert_absent_fields(value: &Value, banned: &[&str]) { let raw = serde_json::to_string(value).unwrap(); for field in banned { assert!( !raw.contains(&format!("\"{field}\"")), "purged field '{field}' present: {raw}" ); } } #[test] fn no_bounty_winner_selection_or_slashing() { let query = serde_json::to_value(QueryBody::new("rust", 5)).unwrap(); assert_exact_keys(&query, &["qid", "text", "entities", "budget"]); assert_exact_keys(query.get("budget").unwrap(), &["max_results"]); let envelope = test_envelope(&Keypair::generate(), TYPE_QUERY, query.clone()); let envelope = serde_json::to_value(&envelope).unwrap(); for value in [&query, &envelope] { assert_absent_fields( value, &[ "bounty", "winner", "winner_selection", "slashing", "stake", "reward", "payment", ], ); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_in_protocol_citation_accounting_or_settlement() { let relay_url = spawn_relay().await; let http = client(); let key = Keypair::generate(); for msg_type in ["receipt", "settlement", "citation", "invoice"] { let envelope = test_envelope(&key, msg_type, serde_json::json!({})); assert_eq!( publish(&http, &relay_url, &envelope).await.status(), reqwest::StatusCode::BAD_REQUEST, "relay accepted {msg_type} on the broadcast channel" ); assert_eq!( unicast(&http, &relay_url, &key.public_hex(), &envelope) .await .status(), reqwest::StatusCode::BAD_REQUEST, "relay accepted unicast {msg_type}" ); } let response = serde_json::to_value(build_response("q1", Vec::new(), Some(0), 5)).unwrap(); assert_absent_fields( &response, &[ "receipt", "settlement", "citation", "cited", "price", "payment", "paid", ], ); let aggregate = serde_json::to_value(AggregateBody { period: "2026-03".to_string(), sent: 0, passed: 0, }) .unwrap(); assert_absent_fields( &aggregate, &[ "receipt", "settlement", "citation", "cited", "price", "payment", "paid", ], ); for route in ["/v1/receipt", "/v1/settlement", "/v1/invoice"] { let response = http .get(format!("{relay_url}{route}")) .send() .await .unwrap(); assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_protocol_query_dedup() { let relay_url = spawn_relay().await; let http = client(); let key = Keypair::generate(); let envelope = test_envelope( &key, TYPE_QUERY, serde_json::json!({"qid": "q1", "text": "rust"}), ); assert!( publish(&http, &relay_url, &envelope) .await .status() .is_success() ); assert!( publish(&http, &relay_url, &envelope) .await .status() .is_success() ); let response = poll(&http, &relay_url, &key, 300).await; let payload: Value = response.json().await.unwrap(); assert_eq!( payload .get("messages") .and_then(Value::as_array) .map(Vec::len), Some(2), "relay deduplicated identical queries: {payload}" ); } #[test] fn no_k_fetch_ingestion_attestations() { let response = build_response("q1", Vec::new(), Some(0), 5); let value = serde_json::to_value(&response).unwrap(); assert_exact_keys( &value, &["qid", "results", "truncated", "more_available", "cursor"], ); assert_absent_fields( &value, &[ "attestation", "attestations", "k_fetch", "fetch_proof", "ingestion_proof", "fetch_count", ], ); } #[test] fn no_result_count_etiquette() { let query = serde_json::to_value(QueryBody::new("rust", 5)).unwrap(); assert_absent_fields( &query, &["min_results", "results_count", "serp", "count_floor"], ); let response = serde_json::to_value(build_response("q1", Vec::new(), Some(0), 5)).unwrap(); assert_absent_fields( &response, &["min_results", "results_count", "serp", "count_floor"], ); } #[test] fn no_global_reputation_score() { let item = ResponseItem { url: "file:///x".to_string(), title: "t".to_string(), summary: String::new(), published: String::new(), exposure: "metadata".to_string(), content: None, }; let item_value = serde_json::to_value(&item).unwrap(); assert_exact_keys( &item_value, &[ "url", "title", "summary", "published", "exposure", "content", ], ); let response_value = serde_json::to_value(build_response("q1", vec![item], Some(1), 5)).unwrap(); for value in [&item_value, &response_value] { assert_absent_fields(value, &["score", "rank", "reputation", "weight", "rating"]); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_aggregate_appeals() { let relay_url = spawn_relay().await; let http = client(); let sender = Keypair::generate(); let member = Keypair::generate(); let dispute = test_envelope( &sender, "dispute", serde_json::json!({"about": "counter", "reason": "unfair"}), ); assert_eq!( publish(&http, &relay_url, &dispute).await.status(), reqwest::StatusCode::BAD_REQUEST ); assert_eq!( unicast(&http, &relay_url, &member.public_hex(), &dispute) .await .status(), reqwest::StatusCode::BAD_REQUEST ); assert_eq!( poll(&http, &relay_url, &member, 30).await.status(), reqwest::StatusCode::NO_CONTENT ); let aggregate = test_envelope( &sender, TYPE_AGGREGATE, serde_json::to_value(AggregateBody { period: "2026-03".to_string(), sent: 1, passed: 1, }) .unwrap(), ); assert_eq!( unicast(&http, &relay_url, &member.public_hex(), &aggregate) .await .status(), reqwest::StatusCode::OK ); let body = serde_json::to_value(AggregateBody { period: "2026-03".to_string(), sent: 1, passed: 1, }) .unwrap(); assert_exact_keys(&body, &["period", "sent", "passed"]); assert_absent_fields(&body, &["appeal", "dispute", "complaint", "sanction"]); for route in ["/v1/appeal", "/v1/dispute"] { let response = http .get(format!("{relay_url}{route}")) .send() .await .unwrap(); assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_topic_channels() { let query = serde_json::to_value(QueryBody::new("rust", 5)).unwrap(); assert_exact_keys(&query, &["qid", "text", "entities", "budget"]); assert_absent_fields( &query, &[ "topic", "channel", "taxonomy", "category", "routing", "topic_id", ], ); let relay_url = spawn_relay().await; let http = client(); let alice = Keypair::generate(); let bob = Keypair::generate(); assert_eq!( poll(&http, &relay_url, &alice, 30).await.status(), reqwest::StatusCode::NO_CONTENT ); assert_eq!( poll(&http, &relay_url, &bob, 30).await.status(), reqwest::StatusCode::NO_CONTENT ); let envelope = test_envelope( &alice, TYPE_QUERY, serde_json::json!({"qid": "q1", "text": "rust"}), ); assert!( publish(&http, &relay_url, &envelope) .await .status() .is_success() ); for member in [&alice, &bob] { let response = poll(&http, &relay_url, &member, 300).await; let payload: Value = response.json().await.unwrap(); assert_eq!( payload .get("messages") .and_then(Value::as_array) .map(Vec::len), Some(1), "receiver-side filtering expected, got {payload}" ); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_broadcast_responses() { let relay_url = spawn_relay().await; let http = client(); let key = Keypair::generate(); for msg_type in [TYPE_RESPONSE, "evidence", "commons", "broadcast_response"] { let envelope = test_envelope(&key, msg_type, serde_json::json!({})); assert_eq!( publish(&http, &relay_url, &envelope).await.status(), reqwest::StatusCode::BAD_REQUEST, "relay accepted broadcast {msg_type}" ); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_normative_query_canonical_form() { let root = tempfile::tempdir().unwrap(); let docs = root.path().join("docs"); fs::create_dir_all(&docs).unwrap(); fs::write(docs.join("alpha.txt"), "rust ownership and borrowing").unwrap(); let config = config_for(&root.path().join("alice"), "alice", "http://127.0.0.1:1"); { let index = LocalIndex::open(&config.index_dir()).unwrap(); index .add_collection(&collection("docs", &docs, true, "full")) .unwrap(); } let node = Node::start(config).await.unwrap(); let text = "rust?! ownership/borrowing: \"quotes\" ünïcode {braces} #tag"; let outcome = node::control_query( &format!("http://{}", node.addr), text, Some(5), Some(100), false, ) .await .unwrap(); assert_eq!(outcome.get("text").and_then(Value::as_str), Some(text)); assert_eq!( outcome.pointer("/local/total").and_then(Value::as_u64), Some(1), "arbitrary phrasing should match lexically: {outcome}" ); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_supply_announce_firehose() { let relay_url = spawn_relay().await; let http = client(); let key = Keypair::generate(); for msg_type in ["announce", "supply", "documents", "publish"] { let envelope = test_envelope(&key, msg_type, serde_json::json!({})); assert_eq!( publish(&http, &relay_url, &envelope).await.status(), reqwest::StatusCode::BAD_REQUEST, "relay accepted supply announcement {msg_type}" ); } for route in ["/v1/announce", "/v1/supply"] { let response = http .get(format!("{relay_url}{route}")) .send() .await .unwrap(); assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn no_durable_replayable_broadcast_stream() { let relay_url = spawn_relay().await; let http = client(); let key = Keypair::generate(); let envelope = test_envelope( &key, TYPE_QUERY, serde_json::json!({"qid": "q1", "text": "rust"}), ); assert!( publish(&http, &relay_url, &envelope) .await .status() .is_success() ); let response = poll(&http, &relay_url, &key, 300).await; let payload: Value = response.json().await.unwrap(); assert_eq!( payload .get("messages") .and_then(Value::as_array) .map(Vec::len), Some(1) ); assert_eq!( poll(&http, &relay_url, &key, 50).await.status(), reqwest::StatusCode::NO_CONTENT, "relay replayed a drained message" ); for route in ["/v1/history", "/v1/replay"] { let response = http .get(format!("{relay_url}{route}")) .send() .await .unwrap(); assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND); } }