#![cfg(not(target_arch = "wasm32"))]
use host_chain_core::chain::ChainId;
use host_chain_core::dotns::{
contenthash_to_cid, decode_abi_bytes, decode_contract_result, decode_scale_compact, hex_encode,
resolve_dotns_with, resolve_owner_with, scale_compact_len, scale_compact_u64,
};
use host_chain_core::identity::{resolve_identity_with, resolve_username_with};
use host_encoding::identity::Credibility;
fn build_contract_result(payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
scale_compact_u64(&mut buf, 0);
buf.push(0x00);
buf.extend_from_slice(&0u128.to_le_bytes());
buf.push(0x00);
buf.extend_from_slice(&0u128.to_le_bytes());
scale_compact_len(&mut buf, 0).expect("compact len 0 must succeed");
buf.extend_from_slice(&0u32.to_le_bytes());
scale_compact_len(&mut buf, payload.len()).expect("payload len compact must succeed");
buf.extend_from_slice(payload);
buf
}
fn build_abi_bytes(data: &[u8]) -> Vec<u8> {
let mut out = vec![0u8; 64];
out[31] = 0x20;
let len = data.len() as u32;
out[60..64].copy_from_slice(&len.to_be_bytes());
out.extend_from_slice(data);
out
}
fn build_ipfs_contenthash(digest: &[u8; 32]) -> Vec<u8> {
let mut ch = vec![0xe3u8, 0x01, 0x01, 0x70, 0x12, 0x20];
ch.extend_from_slice(digest);
ch
}
fn build_cid_contract_result() -> (Vec<u8>, String) {
let digest = [0xabu8; 32];
let contenthash = build_ipfs_contenthash(&digest);
let expected_cid =
contenthash_to_cid(&contenthash).expect("test contenthash must produce a valid CID");
let abi_payload = build_abi_bytes(&contenthash);
let contract_bytes = build_contract_result(&abi_payload);
(contract_bytes, expected_cid)
}
fn build_owner_contract_result(addr: &[u8; 20]) -> Vec<u8> {
let mut abi_word = vec![0u8; 32];
abi_word[12..32].copy_from_slice(addr);
build_contract_result(&abi_word)
}
#[tokio::test]
async fn test_resolve_username_returns_address() {
let hex_payload = format!("0x{}00", "ab".repeat(32));
let resp = format!(r#"{{"jsonrpc":"2.0","id":1,"result":"{hex_payload}"}}"#);
let result = resolve_username_with("alice", ChainId::PaseoPeople, move |_req| {
let resp = resp.clone();
async move { Ok(resp) }
})
.await;
let expected = format!("0x{}", "ab".repeat(32));
assert_eq!(result, Ok(Some(expected)));
}
#[tokio::test]
async fn test_resolve_username_returns_none_for_unknown() {
let result = resolve_username_with("unknown.user", ChainId::PaseoPeople, |_req| async {
Ok(r#"{"jsonrpc":"2.0","id":1,"result":null}"#.to_string())
})
.await;
assert_eq!(result, Ok(None));
}
#[tokio::test]
async fn test_resolve_username_returns_address_polkadot_people() {
let hex_payload = format!("0x{}00", "cd".repeat(32));
let resp = format!(r#"{{"jsonrpc":"2.0","id":1,"result":"{hex_payload}"}}"#);
let result = resolve_username_with("bob", ChainId::PolkadotPeople, move |_req| {
let resp = resp.clone();
async move { Ok(resp) }
})
.await;
let expected = format!("0x{}", "cd".repeat(32));
assert_eq!(result, Ok(Some(expected)));
}
#[tokio::test]
async fn test_resolve_username_propagates_rpc_error() {
let result = resolve_username_with("alice", ChainId::PaseoPeople, |_req| async {
Ok(
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"node overloaded"}}"#
.to_string(),
)
})
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("JSON-RPC error"));
}
#[tokio::test]
async fn test_resolve_username_propagates_transport_failure() {
let result = resolve_username_with("alice", ChainId::PaseoPeople, |_req| async {
Err("connection refused".to_string())
})
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("connection refused"));
}
#[tokio::test]
async fn test_resolve_username_individuality_returns_address() {
let hex_payload = format!("0x{}", "ef".repeat(32));
let resp = format!(r#"{{"jsonrpc":"2.0","id":1,"result":"{hex_payload}"}}"#);
let result = resolve_username_with("carol", ChainId::Individuality, move |_req| {
let resp = resp.clone();
async move { Ok(resp) }
})
.await;
let expected = format!("0x{}", "ef".repeat(32));
assert_eq!(result, Ok(Some(expected)));
}
#[tokio::test]
async fn test_resolve_username_individuality_returns_none_for_unknown() {
let result = resolve_username_with("nobody", ChainId::Individuality, |_req| async {
Ok(r#"{"jsonrpc":"2.0","id":1,"result":null}"#.to_string())
})
.await;
assert_eq!(result, Ok(None));
}
fn build_consumer_info_bytes(lite_username: &str) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&[0x04u8; 65]);
buf.push(0x00);
let name_bytes = lite_username.as_bytes();
buf.push((name_bytes.len() as u8) << 2);
buf.extend_from_slice(name_bytes);
buf.push(0x00);
buf
}
#[tokio::test]
async fn test_resolve_identity_returns_display_name() {
let account_id = format!("0x{}", "ab".repeat(32));
let consumer_bytes = build_consumer_info_bytes("alice");
let hex_payload = hex_encode(&consumer_bytes);
let resp = format!(r#"{{"jsonrpc":"2.0","id":1,"result":"{hex_payload}"}}"#);
let result = resolve_identity_with(&account_id, move |_req| {
let resp = resp.clone();
async move { Ok(resp) }
})
.await;
let info = result.expect("must succeed").expect("slot must be present");
assert_eq!(info.lite_username, "alice");
assert_eq!(info.full_username, None);
assert_eq!(info.credibility, Credibility::Lite);
assert_eq!(info.identifier_key, vec![0x04u8; 65]);
}
#[tokio::test]
async fn test_resolve_identity_returns_none_when_no_identity() {
let account_id = format!("0x{}", "ab".repeat(32));
let result = resolve_identity_with(&account_id, |_req| async {
Ok(r#"{"jsonrpc":"2.0","id":1,"result":null}"#.to_string())
})
.await;
assert_eq!(result, Ok(None));
}
#[tokio::test]
async fn test_resolve_identity_propagates_rpc_error() {
let account_id = format!("0x{}", "ab".repeat(32));
let result = resolve_identity_with(&account_id, |_req| async {
Ok(r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"internal"}}"#.to_string())
})
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("JSON-RPC error"));
}
#[tokio::test]
async fn test_resolve_identity_rejects_invalid_account_id() {
let result = resolve_identity_with("not-a-hex-id", |_req| async {
unreachable!("transport must not be called for an invalid account_id")
})
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid hex"));
}
#[tokio::test]
async fn test_resolve_identity_rejects_wrong_length_account_id() {
let short_id = format!("0x{}", "ab".repeat(16));
let result = resolve_identity_with(&short_id, |_req| async {
unreachable!("transport must not be called for wrong-length account_id")
})
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("32 bytes"));
}
#[tokio::test]
async fn test_resolve_dotns_returns_cid() {
let (contract_bytes, expected_cid) = build_cid_contract_result();
let result = resolve_dotns_with("mytestapp.dot", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
let cid = result.expect("must produce a CID");
assert_eq!(
cid, expected_cid,
"resolved CID must match the constructed fixture"
);
assert!(cid.starts_with('b'), "CIDv1 base32 must start with 'b'");
}
#[tokio::test]
async fn test_resolve_dotns_name_without_dot_suffix_resolves() {
let (contract_bytes, expected_cid) = build_cid_contract_result();
let result = resolve_dotns_with("mytestapp", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
let cid = result.expect("must produce a CID");
assert_eq!(cid, expected_cid);
}
#[tokio::test]
async fn test_resolve_dotns_propagates_transport_failure() {
let result = resolve_dotns_with("fail.dot", |_req| async {
Err("no route to host".to_string())
})
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("no route to host"));
}
#[tokio::test]
async fn test_resolve_dotns_returns_error_for_empty_return_data() {
let contract_bytes = build_contract_result(&[]);
let result = resolve_dotns_with("unregistered.dot", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("domain not registered"));
}
#[tokio::test]
async fn test_resolve_dotns_returns_error_for_zero_length_contenthash() {
let abi_empty = build_abi_bytes(&[]);
let contract_bytes = build_contract_result(&abi_empty);
let result = resolve_dotns_with("nocontent.dot", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("domain has no contenthash set"));
}
#[tokio::test]
async fn test_resolve_owner_returns_account() {
let addr: [u8; 20] = [0xdeu8; 20];
let contract_bytes = build_owner_contract_result(&addr);
let result = resolve_owner_with("mytestapp.dot", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
let owner = result.expect("must return Some(owner)");
assert!(owner.starts_with("0x"), "owner must be 0x-prefixed");
assert_eq!(owner.len(), 42, "0x + 40 hex chars = 42 characters");
assert!(
owner[2..].chars().all(|c| c.is_ascii_hexdigit()),
"owner must be valid hex"
);
}
#[tokio::test]
async fn test_resolve_owner_returns_none_for_zero_address() {
let addr = [0u8; 20];
let contract_bytes = build_owner_contract_result(&addr);
let result = resolve_owner_with("unowned.dot", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
assert_eq!(result, None, "zero address must map to None");
}
#[tokio::test]
async fn test_resolve_owner_returns_none_on_transport_failure() {
let result = resolve_owner_with("fail.dot", |_req| async {
Err("DNS lookup failed".to_string())
})
.await;
assert_eq!(result, None);
}
#[tokio::test]
async fn test_resolve_owner_returns_none_for_short_return_data() {
let contract_bytes = build_contract_result(&[0u8; 16]);
let result = resolve_owner_with("short.dot", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
assert_eq!(result, None);
}
#[tokio::test]
async fn test_resolve_owner_name_without_dot_suffix_resolves() {
let addr: [u8; 20] = [0xabu8; 20];
let contract_bytes = build_owner_contract_result(&addr);
let with_suffix = resolve_owner_with("myapp.dot", {
let bytes = contract_bytes.clone();
move |_req| {
let bytes = bytes.clone();
async move { Ok(bytes) }
}
})
.await;
let without_suffix = resolve_owner_with("myapp", move |_req| {
let bytes = contract_bytes.clone();
async move { Ok(bytes) }
})
.await;
assert_eq!(
with_suffix, without_suffix,
"suffix handling must be transparent to the caller"
);
}
#[test]
fn test_build_contract_result_roundtrips_through_decoder() {
let payload = b"hello world";
let buf = build_contract_result(payload);
let decoded = decode_contract_result(&buf).expect("must decode");
assert_eq!(decoded, payload);
}
#[test]
fn test_build_abi_bytes_roundtrips_through_decoder() {
let data = b"some contenthash bytes";
let encoded = build_abi_bytes(data);
let decoded = decode_abi_bytes(&encoded).expect("must decode");
assert_eq!(decoded, data);
}
#[test]
fn test_build_ipfs_contenthash_produces_valid_cid() {
let digest = [0x42u8; 32];
let contenthash = build_ipfs_contenthash(&digest);
let cid = contenthash_to_cid(&contenthash).expect("must produce CID");
assert!(cid.starts_with('b'), "CIDv1 base32 starts with 'b'");
}
#[test]
fn test_decode_scale_compact_helper_is_accessible() {
let (val, consumed) = decode_scale_compact(&[0x04]).expect("must decode single-byte compact");
assert_eq!(val, 1);
assert_eq!(consumed, 1);
}