use dig_rpc_types::envelope::{JsonRpcResponse, JsonRpcResponseBody};
use dig_rpc_types::error::{ErrorCode, ErrorOrigin, RpcError};
use dig_rpc_types::types::*;
use serde_json::{json, Value};
fn assert_round_trips<T>(raw: Value)
where
T: serde::de::DeserializeOwned + serde::Serialize,
{
let decoded: T = serde_json::from_value(raw.clone())
.unwrap_or_else(|e| panic!("decode failed: {e}\nraw: {raw}"));
let re = serde_json::to_value(&decoded).unwrap();
let obj = raw.as_object().expect("vector must be an object");
for (k, v) in obj {
assert_eq!(&re[k], v, "field {k:?} drifted on round-trip\ngot: {re}");
}
}
#[test]
fn get_content_first_window_node_profile() {
assert_round_trips::<ContentChunk>(json!({
"ciphertext": "AAAA",
"root": "ab".repeat(32),
"complete": false,
"next_offset": 3_145_728u64,
"inclusion_proof": "cHJvb2Y=",
"chunk_lens": [10, 20, 30],
"source": "local",
}));
}
#[test]
fn get_content_network_profile() {
assert_round_trips::<ContentChunk>(json!({
"ciphertext": "AAAA",
"total_length": 100u64,
"offset": 0u64,
"length": 100u64,
"complete": true,
"next_offset": Value::Null,
"inclusion_proof": "cHJvb2Y=",
"chunk_lens": [],
"program_hash": "cd".repeat(32),
"root": "ef".repeat(32),
}));
}
#[test]
fn anchored_root() {
assert_round_trips::<AnchoredRoot>(json!({
"store_id": "ab".repeat(32),
"root": "cd".repeat(32),
}));
}
#[test]
fn network_info() {
assert_round_trips::<NetworkInfo>(json!({
"peer_id": "ab".repeat(32),
"network_id": "DIG_MAINNET",
"listen_addr": "[::1]:9444",
"reflexive_addr": Value::Null,
"candidate_addresses": ["[::1]:9444", "127.0.0.1:9444"],
"reachability": "direct",
"relay": { "url": "wss://relay.dig.net:9450", "reserved": false },
}));
}
#[test]
fn get_peers_empty() {
assert_round_trips::<PeersList>(json!({ "peers": [] }));
}
#[test]
fn announce_ack() {
assert_round_trips::<AnnounceAck>(json!({ "accepted": true, "known_peers": 0 }));
}
#[test]
fn availability_batch_resource_granularity() {
assert_round_trips::<AvailabilityBatch>(json!({
"items": [
{
"available": true,
"total_length": 4096u64,
"chunk_count": 2u64,
"complete": true,
},
{
"available": false,
"providers": [
{ "peer_id": "12".repeat(32),
"addresses": [ { "host": "::1", "port": 9444, "kind": "direct" } ] }
],
}
]
}));
}
#[test]
fn list_inventory_both_shapes() {
assert_round_trips::<Inventory>(json!({
"store_id": "ab".repeat(32),
"roots": ["cd".repeat(32)],
}));
assert_round_trips::<Inventory>(json!({ "stores": ["ef".repeat(32)] }));
}
#[test]
fn fetch_range_first_frame() {
assert_round_trips::<RangeFrame>(json!({
"offset": 0u64,
"length": 1024u64,
"bytes": "AAAA",
"complete": false,
"total_length": 8192u64,
"chunk_lens": [1024, 1024],
"chunk_index": 0u64,
"inclusion_proof": "cHJvb2Y=",
"root": "ab".repeat(32),
}));
}
#[test]
fn fetch_range_later_frame() {
assert_round_trips::<RangeFrame>(json!({
"offset": 1024u64,
"length": 1024u64,
"bytes": "AAAA",
"complete": true,
}));
}
#[test]
fn cache_config() {
assert_round_trips::<CacheConfig>(json!({
"cap_bytes": 1_073_741_824u64,
"used_bytes": 0u64,
"cache_dir": "/home/u/.cache/dig",
"shared": true,
}));
}
#[test]
fn cache_list_cached() {
assert_round_trips::<CachedList>(json!({
"cached": [
{
"capsule": format!("{}:{}", "ab".repeat(32), "cd".repeat(32)),
"store_id": "ab".repeat(32),
"root": "cd".repeat(32),
"size_bytes": 4096u64,
"last_used_unix_ms": 1_700_000_000_000u64,
}
]
}));
}
#[test]
fn cache_fetch_and_cache_variants() {
assert_round_trips::<FetchAndCacheResult>(json!({
"status": "cached",
"size_bytes": 4096u64,
"served_root": "ab".repeat(32),
}));
assert_round_trips::<FetchAndCacheResult>(json!({
"status": "failed",
"message": "upstream miss",
}));
}
#[test]
fn control_peer_status_not_running() {
assert_round_trips::<PeerStatusSnapshot>(json!({
"running": false,
"peer_id": Value::Null,
"network_id": "DIG_MAINNET",
"relay": { "url": "wss://relay.dig.net:9450", "reserved": false },
"connected_peers": 0u64,
"last_error": Value::Null,
}));
}
#[test]
fn content_redirect_error_envelope() {
let raw = json!({
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32008,
"message": "content not held by this node; re-request against a provider in data.redirect",
"data": {
"code": "CONTENT_REDIRECT",
"origin": "node",
"redirect": {
"content": {
"store_id": "ab".repeat(32),
"root": "cd".repeat(32),
"retrieval_key": "ef".repeat(32),
},
"providers": [
{ "peer_id": "12".repeat(32),
"addresses": [ { "host": "::1", "port": 9444, "kind": "direct" } ] }
],
"redirect_depth": 1u64,
"max_redirects": 4u64,
}
}
}
});
let resp: JsonRpcResponse = serde_json::from_value(raw).unwrap();
match resp.body {
JsonRpcResponseBody::Error { error } => {
assert_eq!(error.code, ErrorCode::ContentRedirect);
assert_eq!(error.data.code, "CONTENT_REDIRECT");
assert_eq!(error.data.origin, ErrorOrigin::Node);
let redirect = error.data.redirect.expect("redirect payload");
assert_eq!(redirect.redirect_depth, 1);
assert_eq!(redirect.max_redirects, 4);
assert_eq!(redirect.providers.len(), 1);
assert_eq!(redirect.providers[0].addresses[0].host, "::1");
}
_ => panic!("expected error body"),
}
}
#[test]
fn resource_unavailable_bare_error_upgrades_via_helper() {
let e = RpcError::of(ErrorCode::ResourceUnavailable, "not held");
let v = serde_json::to_value(&e).unwrap();
assert_eq!(v["code"], -32004);
assert_eq!(v["data"]["code"], "RESOURCE_UNAVAILABLE");
assert_eq!(v["data"]["origin"], "node");
}
#[test]
fn stage_result() {
assert_round_trips::<StageResult>(json!({
"capsule": format!("{}:{}", "ab".repeat(32), "cd".repeat(32)),
"store_id": "ab".repeat(32),
"root": "cd".repeat(32),
"module_path": "/tmp/x.dig",
"size": 8192u64,
"content_address": format!("chia://{}:{}/", "ab".repeat(32), "cd".repeat(32)),
"files": ["index.html", "style.css"],
"ephemeral": false,
}));
}