444 lines
13 KiB
Rust
444 lines
13 KiB
Rust
mod common;
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::fs;
|
|
|
|
use common::{client, collection, config_for, poll, publish, spawn_relay, unicast};
|
|
use frxd::crypto::Keypair;
|
|
use frxd::index::LocalIndex;
|
|
use frxd::message::{
|
|
AggregateBody, Envelope, 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 = Envelope::new(&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 = Envelope::new(&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(), 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 = Envelope::new(
|
|
&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(), 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(), 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], 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 = Envelope::new(
|
|
&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 = Envelope::new(
|
|
&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 = Envelope::new(
|
|
&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 = Envelope::new(&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 = Envelope::new(&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 = Envelope::new(
|
|
&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);
|
|
}
|
|
}
|