mod common;
use common::payload::{payload, round_trip_by_name, Fill, DISPATCHED, EXCLUDED};
use rustigram_types::rich_message;
use serde_json::{json, Value};
#[test]
fn every_spec_type_round_trips() {
let spec = common::load();
let mut failures = Vec::new();
for (name, fields) in &spec.types {
if EXCLUDED.iter().any(|(excluded, _)| excluded == name) {
continue;
}
let payload = match payload(&spec, name, Fill::Required, 0) {
Ok(payload) => payload,
Err(why) => {
failures.push(format!(" {name}: could not build a payload — {why}"));
continue;
}
};
let encoded = match round_trip_by_name(name, &payload) {
Ok(encoded) => encoded,
Err(why) => {
failures.push(format!(" {name}: {why}"));
continue;
}
};
let Some(object) = encoded.as_object() else {
failures.push(format!(
" {name}: re-encoded as {encoded}, which is not a JSON object"
));
continue;
};
let lost: Vec<&str> = fields
.iter()
.filter(|(field, spec_field)| !spec_field.optional() && !object.contains_key(*field))
.map(|(field, _)| field.as_str())
.collect();
if !lost.is_empty() {
failures.push(format!(
" {name}: required field(s) {lost:?} did not survive the round trip.\n\
\x20 sent: {payload}\n\
\x20 came back: {encoded}"
));
}
}
assert!(
failures.is_empty(),
"{} of {} spec types failed to round trip:\n{}",
failures.len(),
spec.types.len(),
failures.join("\n")
);
}
#[test]
fn the_dispatch_table_covers_every_spec_type() {
let spec = common::load();
let missing: Vec<&str> = spec
.types
.keys()
.map(String::as_str)
.filter(|name| !DISPATCHED.contains(name) && !EXCLUDED.iter().any(|(e, _)| e == name))
.collect();
assert!(
missing.is_empty(),
"{} spec type(s) are in neither the dispatch table nor the exclusion \
list, so nothing round-trips them:\n {}",
missing.len(),
missing.join("\n ")
);
let unknown: Vec<&&str> = DISPATCHED
.iter()
.filter(|name| !spec.types.contains_key(**name))
.collect();
assert!(
unknown.is_empty(),
"{} dispatch entr(ies) name a type the spec no longer has — remove \
them:\n {unknown:?}",
unknown.len()
);
let mut sorted = DISPATCHED.to_vec();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
sorted.len(),
DISPATCHED.len(),
"the dispatch table lists a type twice"
);
}
#[test]
fn every_exclusion_still_applies() {
let spec = common::load();
let stale: Vec<&str> = EXCLUDED
.iter()
.map(|(name, _)| *name)
.filter(|name| !spec.types.contains_key(*name))
.collect();
assert!(
stale.is_empty(),
"{} exclusion(s) name a type the spec no longer has — remove them: {stale:?}",
stale.len()
);
}
#[test]
fn rich_block_map_keeps_its_nested_location() {
let encoded = serde_json::to_value(rich_message::RichBlock::Map(
serde_json::from_value(json!({
"location": { "latitude": 41.0, "longitude": 29.0 },
"zoom": 12, "width": 640, "height": 480
}))
.expect("a map block decodes from a nested location"),
))
.expect("a map block re-encodes");
assert!(
encoded.get("location").is_some(),
"the map block lost its nested `location` object; flattening it back to \
`latitude`/`longitude` is what stopped this type decoding: {encoded}"
);
assert!(
encoded.get("latitude").is_none(),
"coordinates leaked back to the top level: {encoded}"
);
}
#[test]
fn rich_text_reference_keeps_its_name_field() {
let node: rich_message::RichTextNode =
serde_json::from_value(json!({ "type": "reference", "text": "see also", "name": "rfc-1" }))
.expect("a reference node decodes from the spec's wire shape");
let encoded = serde_json::to_value(&node).expect("a reference node re-encodes");
assert_eq!(
encoded.get("name").and_then(Value::as_str),
Some("rfc-1"),
"the reference lost its `name` key — Telegram would not see it: {encoded}"
);
}