CultLib/packages/cultnet-rs/tests/decode_door.rs
Meta Vens 0daa2f1017 cultnet-rs: kind-array refusal no longer quotes the value; README says delete carries ids
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:55:05 +02:00

717 lines
26 KiB
Rust

//! Every MessagePack byte cultnet-rs receives decodes once through `cultcache_rs::decode_payload`:
//! a message, the row arrays of `snapshot_response_raw.v1`, a raw document payload applied to a cache,
//! and an immutable identity credential. Each test decodes in a child test process on a thread of a
//! stated size, so an abort fails one named test; values are built, encoded and compared on a large
//! thread, and only the decode runs on the small one.
use anyhow::Result;
use cultcache_rs::CultCache;
use cultcache_rs::CultSlots;
use cultcache_rs::DatabaseEntry;
use cultcache_rs::MAX_PAYLOAD_DEPTH;
use cultcache_rs::PayloadTooDeep;
use cultnet_rs::*;
const CHILD_MARKER: &str = "CULTNET_DECODE_DOOR_CHILD";
const SMALL: usize = 256 * 1024;
const BIG: usize = 64 * 1024 * 1024;
/// A five-byte MessagePack string standing where a test wants its nested value spliced in.
const SENTINEL: [u8; 5] = [0xa4, b'D', b'E', b'E', b'P'];
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct Wide32 {
#[cultcache(key = 0)]
a0: [u64; 32],
#[cultcache(key = 1)]
a1: [u64; 32],
#[cultcache(key = 2)]
a2: [u64; 32],
#[cultcache(key = 3)]
a3: [u64; 32],
#[cultcache(key = 4)]
a4: [u64; 32],
#[cultcache(key = 5)]
a5: [u64; 32],
#[cultcache(key = 6)]
a6: [u64; 32],
#[cultcache(key = 7)]
a7: [u64; 32],
#[cultcache(key = 8)]
a8: [u64; 32],
#[cultcache(key = 9)]
a9: [u64; 32],
#[cultcache(key = 10)]
a10: [u64; 32],
#[cultcache(key = 11)]
a11: [u64; 32],
#[cultcache(key = 12)]
a12: [u64; 32],
#[cultcache(key = 13)]
a13: [u64; 32],
#[cultcache(key = 14)]
a14: [u64; 32],
#[cultcache(key = 15)]
a15: [u64; 32],
#[cultcache(key = 16)]
a16: [u64; 32],
#[cultcache(key = 17)]
a17: [u64; 32],
#[cultcache(key = 18)]
a18: [u64; 32],
#[cultcache(key = 19)]
a19: [u64; 32],
#[cultcache(key = 20)]
a20: [u64; 32],
#[cultcache(key = 21)]
a21: [u64; 32],
#[cultcache(key = 22)]
a22: [u64; 32],
#[cultcache(key = 23)]
a23: [u64; 32],
#[cultcache(key = 24)]
a24: [u64; 32],
#[cultcache(key = 25)]
a25: [u64; 32],
#[cultcache(key = 26)]
a26: [u64; 32],
#[cultcache(key = 27)]
a27: [u64; 32],
#[cultcache(key = 28)]
a28: [u64; 32],
#[cultcache(key = 29)]
a29: [u64; 32],
#[cultcache(key = 30)]
a30: [u64; 32],
#[cultcache(key = 31)]
a31: [u64; 32],
#[cultcache(key = 32)]
next: Option<Box<Wide32>>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "cultnet.decode-door.wide", schema = "DecodeDoorWide")]
struct WideDocument {
#[cultcache(key = 0)]
chain: Wide32,
}
fn on_stack<R: Send + 'static>(bytes: usize, body: impl FnOnce() -> R + Send + 'static) -> R {
std::thread::Builder::new().stack_size(bytes).spawn(body).unwrap().join().unwrap()
}
/// Runs `body` in a child process that runs only the test `name`; the parent passes when the child
/// exits cleanly having run exactly that one test. An abort in the child fails the parent.
fn in_child(name: &str, body: impl FnOnce()) {
if std::env::var_os(CHILD_MARKER).is_some() {
body();
return;
}
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([name, "--exact", "--nocapture", "--test-threads=1"])
.env(CHILD_MARKER, "1")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success() && stdout.contains("1 passed"),
"child {name} failed: {:?}\n{stdout}\n{stderr}",
output.status
);
}
/// `levels` one-element arrays around a nil, from marker bytes: the encoders recurse too.
fn nested(levels: usize) -> Vec<u8> {
let mut bytes = vec![0x91; levels];
bytes.push(0xc0);
bytes
}
/// Swaps the one sentinel string in `bytes` for `with`.
fn splice(bytes: Vec<u8>, with: &[u8]) -> Vec<u8> {
let at = bytes
.windows(SENTINEL.len())
.position(|window| window == SENTINEL)
.expect("sentinel present");
[&bytes[..at], with, &bytes[at + SENTINEL.len()..]].concat()
}
fn map_with_sentinel(mut entries: Vec<(rmpv::Value, rmpv::Value)>) -> Vec<u8> {
entries.push(("x".into(), "DEEP".into()));
rmp_serde::to_vec(&rmpv::Value::Map(entries)).unwrap()
}
fn hello_with(unknown: &[u8]) -> Vec<u8> {
splice(
map_with_sentinel(vec![
("schemaVersion".into(), "cultnet.hello.v0".into()),
("runtimeId".into(), "rt".into()),
("runtimeKind".into(), "test".into()),
]),
unknown,
)
}
fn decode_message(bytes: Vec<u8>, stack: usize) -> Result<CultNetMessage> {
*on_stack(stack, move || {
Box::new(decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0))
})
}
#[test]
fn generic_message_with_deep_unknown_key_decodes_on_a_small_thread() {
in_child("generic_message_with_deep_unknown_key_decodes_on_a_small_thread", || {
let plain = decode_message(hello_with(&nested(0)), SMALL).unwrap();
let deep = decode_message(hello_with(&nested(MAX_PAYLOAD_DEPTH - 2)), SMALL).unwrap();
assert_eq!(deep, plain);
});
}
#[test]
fn a_decoded_message_that_fails_validation_is_refused() {
let bytes = splice(
map_with_sentinel(vec![
("schemaVersion".into(), "cultnet.hello.v0".into()),
("runtimeId".into(), "".into()),
("runtimeKind".into(), "test".into()),
]),
&nested(0),
);
let error = decode_message(bytes, BIG).unwrap_err();
assert!(format!("{error:#}").contains("runtimeId"), "{error:#}");
}
#[test]
fn message_past_the_bound_is_refused_typed() {
in_child("message_past_the_bound_is_refused_typed", || {
let error = decode_message(hello_with(&nested(MAX_PAYLOAD_DEPTH + 1)), SMALL).unwrap_err();
let refusal = error.downcast_ref::<PayloadTooDeep>().expect("typed refusal");
assert_eq!(refusal.limit, MAX_PAYLOAD_DEPTH);
});
}
fn raw_v1_bytes(header_unknown: Option<&[u8]>) -> Vec<u8> {
let mut header: Vec<(rmpv::Value, rmpv::Value)> = vec![
("schemaId".into(), "s".into()),
("recordKey".into(), "k".into()),
("storedAt".into(), "2026-10-16T00:00:00Z".into()),
];
if header_unknown.is_some() {
header.push(("x".into(), "DEEP".into()));
}
let message = rmpv::Value::Map(vec![
("schemaVersion".into(), "cultnet.snapshot_response_raw.v1".into()),
("messageId".into(), "m".into()),
("matched".into(), 1.into()),
("asOf".into(), 7.into()),
("headers".into(), rmpv::Value::Array(vec![rmpv::Value::Map(header)])),
]);
let bytes = rmp_serde::to_vec(&message).unwrap();
match header_unknown {
Some(nested) => splice(bytes, nested),
None => bytes,
}
}
#[test]
fn raw_v1_rows_with_deep_unknown_key_decode_on_a_small_thread() {
in_child("raw_v1_rows_with_deep_unknown_key_decode_on_a_small_thread", || {
let plain = decode_message(raw_v1_bytes(None), SMALL).unwrap();
// The message map, the headers array and the header map sit above the unknown key.
let deep = decode_message(raw_v1_bytes(Some(&nested(MAX_PAYLOAD_DEPTH - 3))), SMALL).unwrap();
let CultNetMessage::SnapshotResponseRawV1 { headers: Some(rows), .. } = &deep else {
panic!("expected raw v1 rows");
};
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].record_key, "k");
assert_eq!(deep, plain);
});
}
fn wide_chain(levels: usize) -> Wide32 {
let mut chain: Option<Box<Wide32>> = None;
for level in (0..levels).rev() {
let mut node = Wide32 { next: chain, ..empty_wide() };
node.a0[0] = level as u64;
chain = Some(Box::new(node));
}
*chain.unwrap()
}
fn empty_wide() -> Wide32 {
Wide32 {
a0: [0; 32],
a1: [0; 32],
a2: [0; 32],
a3: [0; 32],
a4: [0; 32],
a5: [0; 32],
a6: [0; 32],
a7: [0; 32],
a8: [0; 32],
a9: [0; 32],
a10: [0; 32],
a11: [0; 32],
a12: [0; 32],
a13: [0; 32],
a14: [0; 32],
a15: [0; 32],
a16: [0; 32],
a17: [0; 32],
a18: [0; 32],
a19: [0; 32],
a20: [0; 32],
a21: [0; 32],
a22: [0; 32],
a23: [0; 32],
a24: [0; 32],
a25: [0; 32],
a26: [0; 32],
a27: [0; 32],
a28: [0; 32],
a29: [0; 32],
a30: [0; 32],
a31: [0; 32],
next: None,
}
}
fn registry() -> CultNetDocumentRegistry {
let mut registry = CultNetDocumentRegistry::new();
registry.register(CultNetDocumentBinding::for_entry::<WideDocument>("cultnet.decode-door.wide".to_string()));
registry
}
/// The wire bytes of a raw put of a `levels`-deep Wide32 document, built on a large thread.
fn raw_put_bytes(levels: usize) -> Vec<u8> {
on_stack(BIG, move || {
let document = WideDocument { chain: wide_chain(levels) };
let message = registry()
.create_raw_document_put_message("m", "k", &document, CultNetDocumentPutOptions::default())
.unwrap();
encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0).unwrap()
})
}
/// Receives and applies on the small thread; the applied record is boxed there.
fn receive_and_apply(bytes: Vec<u8>) -> Result<WideDocument> {
*on_stack(SMALL, move || {
Box::new((|| {
let message = decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0)?;
let mut cache = CultCache::new();
cache.register_entry_type::<WideDocument>()?;
registry().apply_raw_document_put_message::<WideDocument>(&mut cache, &message)
})())
})
}
#[test]
fn raw_put_of_a_wide32_chain_applies_on_a_small_thread() {
in_child("raw_put_of_a_wide32_chain_applies_on_a_small_thread", || {
let levels = MAX_PAYLOAD_DEPTH - 2;
let applied = receive_and_apply(raw_put_bytes(levels)).unwrap();
on_stack(BIG, move || assert!(applied.chain == wide_chain(levels)));
let error = receive_and_apply(raw_put_bytes(levels + 1)).unwrap_err();
assert!(error.downcast_ref::<PayloadTooDeep>().is_some(), "typed refusal");
});
}
#[test]
fn retired_dynamic_put_is_refused_typed() {
in_child("retired_dynamic_put_is_refused_typed", || {
let document = rmpv::Value::Map(vec![
("schemaId".into(), "s".into()),
("recordKey".into(), "k".into()),
("storedAt".into(), "2026-10-16T00:00:00Z".into()),
("payload".into(), "DEEP".into()),
]);
let bytes = splice(
rmp_serde::to_vec(&rmpv::Value::Map(vec![
("schemaVersion".into(), "cultnet.document_put.v0".into()),
("messageId".into(), "m".into()),
("document".into(), document),
]))
.unwrap(),
&nested(253),
);
let error = decode_message(bytes, SMALL).unwrap_err();
// The refusal names the schema the sender spelled, shaped like one, and nothing else of it.
assert!(format!("{error:#}").contains("cultnet.document_put.v0"), "{error:#}");
});
}
#[test]
fn identity_credential_with_deep_key_is_refused_on_a_small_thread() {
in_child("identity_credential_with_deep_key_is_refused_on_a_small_thread", || {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("identity.cc");
enroll_service_identity_at::<IdunnServiceIdentity>(&path).unwrap();
let mut envelopes: rmpv::Value = rmp_serde::from_slice(&std::fs::read(path).unwrap()).unwrap();
let rmpv::Value::Array(items) = &mut envelopes else {
panic!("credential is an array");
};
// The writer encodes an envelope positionally; the reader also takes the named map form,
// where an unknown key is skipped, so the test spells the envelope that way plus one key.
let rmpv::Value::Array(slots) = std::mem::replace(&mut items[0], rmpv::Value::Nil) else {
panic!("envelope is a positional array");
};
let mut named: Vec<(rmpv::Value, rmpv::Value)> = ["key", "type", "payload", "storedAt", "schemaId"]
.into_iter()
.map(rmpv::Value::from)
.zip(slots)
.collect();
named.push(("zz".into(), "DEEP".into()));
items[0] = rmpv::Value::Map(named);
let credential = rmp_serde::to_vec(&envelopes).unwrap();
for levels in [1000, 250] {
let bytes = splice(credential.clone(), &nested(levels));
let refused = on_stack(SMALL, move || {
open_service_identity_credential_bytes::<IdunnServiceIdentity>(&bytes).is_err()
});
assert!(refused, "{levels} levels deep");
}
});
}
type Entries = Vec<(rmpv::Value, rmpv::Value)>;
fn pairs(entries: &[(&str, rmpv::Value)]) -> Entries {
entries.iter().map(|(key, value)| ((*key).into(), value.clone())).collect()
}
fn encoded(entries: Entries) -> Vec<u8> {
rmp_serde::to_vec(&rmpv::Value::Map(entries)).unwrap()
}
/// The refusal text of a message built from `entries`, as every receiver prints it.
fn refusal(entries: Entries) -> String {
format!("{:#}", decode_message(encoded(entries), BIG).unwrap_err())
}
fn wire_record(extra: &[(&str, rmpv::Value)]) -> rmpv::Value {
let mut entries = pairs(&[
("schemaId", "s".into()),
("recordKey", "k".into()),
("storedAt", "2026-10-16T00:00:00Z".into()),
("payloadEncoding", "messagepack".into()),
("payload", rmpv::Value::Binary(vec![0x90])),
]);
entries.extend(pairs(extra));
rmpv::Value::Map(entries)
}
fn raw_record() -> CultNetRawDocumentRecord {
CultNetRawDocumentRecord {
schema_id: "s".into(),
record_key: "k".into(),
stored_at: "2026-10-16T00:00:00Z".into(),
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
payload: vec![0x90],
source_runtime_id: None,
source_agent_id: None,
source_role: None,
tags: None,
}
}
fn sv(name: &str) -> (&str, rmpv::Value) {
("schemaVersion", rmpv::Value::from(name))
}
/// One valid message of every family the hand reader owns.
fn raw_family_messages() -> Vec<CultNetMessage> {
vec![
CultNetMessage::DocumentPutRaw { message_id: "m".into(), document: raw_record() },
CultNetMessage::SnapshotResponseRaw { message_id: "m".into(), documents: vec![raw_record()] },
CultNetMessage::ContentChunkRequest {
message_id: "m".into(),
chunk_hash: "h".into(),
record_key: "k".into(),
expected_size_bytes: 1,
},
CultNetMessage::ContentChunkResponse {
message_id: "m".into(),
found: true,
chunk_hash: "h".into(),
size_bytes: 1,
payload: vec![1],
error: String::new(),
},
CultNetMessage::Error { error: "e".into(), code: None, details: None },
]
}
#[test]
fn every_raw_family_has_exactly_one_decoder() {
for message in raw_family_messages() {
let bytes = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0).unwrap();
// The derive cannot decode a raw-owned family: only the hand reader can.
assert!(cultcache_rs::decode_payload::<CultNetMessage>(&bytes).is_err(), "{message:?}");
let decoded = decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0).unwrap();
assert_eq!(decoded, message);
}
// v1 belongs to the derive alone: it decodes the bytes, and the door returns the same message.
let v1 = CultNetMessage::SnapshotResponseRawV1 {
message_id: "m".into(),
matched: 1,
as_of: 7,
next: None,
headers: Some(vec![]),
documents: None,
edges: None,
shard_id: None,
shard_epoch: None,
shard_log_sequence: None,
};
let bytes = encode_cultnet_message_to_vec(&v1, CultNetWireContract::CultNetSchemaV0).unwrap();
assert_eq!(cultcache_rs::decode_payload::<CultNetMessage>(&bytes).unwrap(), v1);
assert_eq!(
decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0).unwrap(),
v1
);
}
#[test]
fn a_duplicated_key_is_refused_in_every_family() {
let bases: Vec<(&str, Vec<(&str, rmpv::Value)>)> = vec![
("put_raw", vec![sv("cultnet.document_put_raw.v0"), ("messageId", "m".into()), ("document", wire_record(&[]))]),
(
"snapshot_raw",
vec![
sv("cultnet.snapshot_response_raw.v0"),
("messageId", "m".into()),
("documents", rmpv::Value::Array(vec![wire_record(&[])])),
],
),
(
"chunk_request",
vec![
sv("cultmesh.content_chunk_request.v1"),
("messageId", "m".into()),
("chunkHash", "h".into()),
("recordKey", "k".into()),
("expectedSizeBytes", 1.into()),
],
),
(
"chunk_response",
vec![
sv("cultmesh.content_chunk_response.v1"),
("messageId", "m".into()),
("found", true.into()),
("chunkHash", "h".into()),
("sizeBytes", 1.into()),
("payload", rmpv::Value::Binary(vec![1])),
("error", "".into()),
],
),
("error", vec![sv("cultnet.error.v0"), ("error", "e".into())]),
(
"v1",
vec![
sv("cultnet.snapshot_response_raw.v1"),
("messageId", "m".into()),
("matched", 1.into()),
("asOf", 7.into()),
("headers", rmpv::Value::Array(vec![])),
],
),
("hello", vec![sv("cultnet.hello.v0"), ("runtimeId", "rt".into()), ("runtimeKind", "test".into())]),
];
for (label, base) in &bases {
decode_message(encoded(pairs(base)), BIG).unwrap_or_else(|error| panic!("{label} control: {error:#}"));
// Each key in turn spelled a second time, with a different value.
for index in 0..base.len() {
let mut doubled = base.clone();
doubled.push((base[index].0, "second".into()));
let text = refusal(pairs(&doubled));
assert!(
text.contains("twice") || text.contains("duplicate field"),
"{label} {}: {text}",
base[index].0
);
}
}
// A key spelled twice one level down: in a document record (both record lanes) and in error details.
let doubled_record = wire_record(&[("schemaId", "t".into())]);
for entries in [
pairs(&[sv("cultnet.document_put_raw.v0"), ("messageId", "m".into()), ("document", doubled_record.clone())]),
pairs(&[
sv("cultnet.snapshot_response_raw.v0"),
("messageId", "m".into()),
("documents", rmpv::Value::Array(vec![doubled_record.clone()])),
]),
] {
let text = refusal(entries);
assert!(text.contains("twice"), "{text}");
}
let error_with = |details: Entries| {
pairs(&[
sv("cultnet.error.v0"),
("error", "e".into()),
("code", "selection_invalid".into()),
("details", rmpv::Value::Map(details)),
])
};
decode_message(encoded(error_with(pairs(&[("field", "f".into())]))), BIG).unwrap();
let text = refusal(error_with(pairs(&[("field", "f".into()), ("field", "g".into())])));
assert!(text.contains("twice"), "{text}");
// v1 rows come from the derive, which refuses a key spelled twice inside a row.
let v1_header = |extra: &[(&str, rmpv::Value)]| {
let mut header = pairs(&[
("schemaId", "s".into()),
("recordKey", "k".into()),
("storedAt", "2026-10-16T00:00:00Z".into()),
]);
header.extend(pairs(extra));
pairs(&[
sv("cultnet.snapshot_response_raw.v1"),
("messageId", "m".into()),
("matched", 1.into()),
("asOf", 7.into()),
("headers", rmpv::Value::Array(vec![rmpv::Value::Map(header)])),
])
};
decode_message(encoded(v1_header(&[])), BIG).unwrap();
let text = refusal(v1_header(&[("recordKey", "second".into())]));
assert!(text.contains("duplicate field"), "{text}");
}
#[test]
fn the_schema_version_spelled_twice_is_refused_whichever_family_comes_first() {
for versions in [["cultnet.hello.v0", "cultnet.error.v0"], ["cultnet.error.v0", "cultnet.hello.v0"]] {
let entries = pairs(&[
sv(versions[0]),
sv(versions[1]),
("runtimeId", "rt".into()),
("runtimeKind", "test".into()),
("error", "e".into()),
]);
decode_message(encoded(entries), BIG).unwrap_err();
}
}
#[test]
fn receive_errors_name_schema_never_values() {
let canary = "CANARY-5e1f";
let hello = |extra: &[(&str, rmpv::Value)]| {
let mut entries = vec![sv("cultnet.hello.v0"), ("runtimeKind", "test".into())];
entries.extend(extra.iter().cloned());
pairs(&entries)
};
let cases: Vec<(&str, Entries, &str)> = vec![
("unknown schema", pairs(&[sv("cultnet.bogus.v0"), ("messageId", "m".into())]), "cultnet.bogus.v0"),
("missing runtimeId", hello(&[]), "runtimeId"),
(
"mistyped bool",
hello(&[("runtimeId", "rt".into()), ("supportsSchemaCatalog", canary.into())]),
"cultnet.hello.v0",
),
(
"login without auth",
pairs(&[sv("cultnet.login.v0"), ("nonce", "n".into()), ("password", canary.into())]),
"auth",
),
(
"unrecognized error code",
pairs(&[sv("cultnet.error.v0"), ("error", "e".into()), ("code", canary.into())]),
"cultnet.error.v0",
),
(
"unknown payload encoding",
pairs(&[
sv("cultnet.document_put_raw.v0"),
("messageId", "m".into()),
(
"document",
rmpv::Value::Map(pairs(&[
("schemaId", "s".into()),
("recordKey", "k".into()),
("storedAt", "2026-10-16T00:00:00Z".into()),
("payloadEncoding", canary.into()),
("payload", rmpv::Value::Binary(vec![0x90])),
])),
),
]),
"payloadEncoding",
),
("schemaVersion that is no schema name", pairs(&[sv(canary), ("messageId", "m".into())]), "schemaVersion"),
];
for (label, entries, names) in cases {
let text = refusal(entries);
assert!(text.contains(names), "{label}: {text}");
assert!(!text.contains("CANARY") && !text.contains("canary"), "{label}: {text}");
}
// A message with no schemaVersion at all says so.
assert!(refusal(pairs(&[("messageId", "m".into())])).contains("no schemaVersion"));
}
#[test]
fn a_networking_schema_descriptor_with_a_repeated_key_is_refused_and_a_bad_kind_is_not_quoted() {
let canary = "CANARY-8c2d";
let descriptor = |extra: &[(&str, rmpv::Value)]| {
let mut entries = pairs(&[("schemaId", "s".into()), ("kind", "wire_message".into()), ("contentHash", "h".into())]);
entries.extend(pairs(extra));
rmpv::Value::Map(entries)
};
let response = |descriptor: rmpv::Value| {
let union = rmpv::Value::Array(vec![
8.into(),
rmpv::Value::Array(vec!["m".into(), rmpv::Value::Array(vec![descriptor])]),
]);
rmp_serde::to_vec(&union).unwrap()
};
let decode = |bytes: Vec<u8>| {
on_stack(BIG, move || {
decode_cultnet_message_from_slice(&bytes, CultNetWireContract::GameCultNetworkingV0)
.map_err(|error| format!("{error:#}"))
})
};
decode(response(descriptor(&[]))).expect("the control decodes");
let text = decode(response(descriptor(&[("schemaId", "t".into())]))).unwrap_err();
assert!(text.contains("twice"), "{text}");
let bad_kind = rmpv::Value::Map(pairs(&[("schemaId", "s".into()), ("kind", canary.into()), ("contentHash", "h".into())]));
let text = decode(response(bad_kind)).unwrap_err();
assert!(text.contains("Kind") && !text.contains("CANARY"), "{text}");
}
#[test]
fn a_networking_catalog_request_with_a_bad_kind_element_is_refused_without_quoting_it() {
let union = rmpv::Value::Array(vec![
7.into(),
rmpv::Value::Array(vec![
"m".into(),
false.into(),
rmpv::Value::Nil,
rmpv::Value::Array(vec!["CANARY-s2x".into()]),
]),
]);
let bytes = rmp_serde::to_vec(&union).unwrap();
let text = on_stack(BIG, move || {
decode_cultnet_message_from_slice(&bytes, CultNetWireContract::GameCultNetworkingV0)
.map_err(|error| format!("{error:#}"))
})
.unwrap_err();
assert!(text.contains("Kinds") && !text.contains("CANARY"), "{text}");
}
#[test]
fn a_schema_version_that_is_not_shaped_like_a_schema_name_is_described_by_length() {
let long = format!("cultnet.{}.v0", "a".repeat(100));
for version in [
"CANARY-5e1f",
"1canary.v0",
"cultnet.CANARY.v0",
"cultnet..v0",
"canary",
"cultnet.canary-x.v0",
long.as_str(),
] {
let text = refusal(pairs(&[sv(version), ("messageId", "m".into())]));
assert!(text.contains(&format!("is not a schema name ({} bytes)", version.len())), "{version}: {text}");
assert!(!text.contains(version), "{version}: {text}");
}
// Shaped like a schema name, it is named (the sender spelled a schema, not data).
let text = refusal(pairs(&[sv("cultnet.x_y9.v10"), ("messageId", "m".into())]));
assert!(text.contains("cultnet.x_y9.v10 refused"), "{text}");
}