ead48a824e
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
120 lines
5.1 KiB
Rust
120 lines
5.1 KiB
Rust
//! Malformed input, size caps and diagnostics: errors and diagnostics only, never a panic.
|
|
|
|
use notescraft_canvas::{Canvas, Color, Diagnostic, Error, MAX_BYTES, MAX_NODES};
|
|
|
|
fn err(input: &str) -> Error {
|
|
match Canvas::from_str(input) {
|
|
Ok(_) => panic!("expected an error for: {input}"),
|
|
Err(e) => e,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_json_is_an_error() {
|
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/truncated.canvas");
|
|
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("fixture: {e}"));
|
|
assert!(matches!(err(&text), Error::Json(_)));
|
|
assert!(matches!(err("{\"nodes\": ["), Error::Json(_)));
|
|
assert!(matches!(err(""), Error::Json(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn root_must_be_an_object() {
|
|
assert!(matches!(err("[]"), Error::NotAnObject { .. }));
|
|
assert!(matches!(err("\"canvas\""), Error::NotAnObject { .. }));
|
|
assert!(matches!(err("42"), Error::NotAnObject { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_types_are_errors() {
|
|
assert!(matches!(err("{\"nodes\": {}}"), Error::NotAnArray { .. }));
|
|
assert!(matches!(err("{\"edges\": 3}"), Error::NotAnArray { .. }));
|
|
assert!(matches!(err("{\"nodes\": [3]}"), Error::NotAnObject { .. }));
|
|
assert!(matches!(err("{\"nodes\": [{\"id\": 3, \"type\": \"text\"}]}"), Error::BadType { .. }));
|
|
assert!(matches!(err("{\"nodes\": [{\"type\": \"text\"}]}"), Error::MissingField { .. }));
|
|
assert!(matches!(err("{\"nodes\": [{\"id\": \"a\"}]}"), Error::MissingField { .. }));
|
|
assert!(matches!(err("{\"nodes\": [{\"id\": \"a\", \"type\": \"text\", \"x\": \"0\", \"y\": 0, \"width\": 1, \"height\": 1}]}"), Error::BadType { .. }));
|
|
assert!(matches!(
|
|
err("{\"nodes\": [{\"id\": \"a\", \"type\": \"text\", \"x\": 0, \"y\": 0, \"width\": 1, \"height\": 1, \"text\": 5}]}"),
|
|
Error::BadType { .. }
|
|
));
|
|
assert!(matches!(
|
|
err("{\"nodes\": [{\"id\": \"a\", \"type\": \"text\", \"x\": 0, \"y\": 0, \"width\": 1, \"height\": 1, \"color\": 2}]}"),
|
|
Error::BadType { .. }
|
|
));
|
|
// unknown keys keep any value shape at all
|
|
assert!(
|
|
Canvas::from_str("{\"nodes\": [{\"id\": \"a\", \"type\": \"text\", \"x\": 0, \"y\": 0, \"width\": 1, \"height\": 1, \"unknown\": [\"a\", 2]}]}")
|
|
.is_ok()
|
|
);
|
|
assert!(matches!(err("{\"edges\": [{\"id\": \"e\", \"fromNode\": \"a\"}]}"), Error::MissingField { .. }));
|
|
assert!(matches!(err("{\"edges\": [{\"id\": \"e\", \"fromNode\": \"a\", \"toNode\": \"b\", \"fromSide\": \"up\"}]}"), Error::BadValue { .. }));
|
|
assert!(matches!(err("{\"edges\": [{\"id\": \"e\", \"fromNode\": \"a\", \"toNode\": \"b\", \"toEnd\": \"triangle\"}]}"), Error::BadValue { .. }));
|
|
assert!(matches!(
|
|
err("{\"nodes\": [{\"id\": \"a\", \"type\": \"group\", \"x\": 0, \"y\": 0, \"width\": 1, \"height\": 1, \"backgroundStyle\": \"zoom\"}]}"),
|
|
Error::BadValue { .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn bad_color_is_kept_and_flagged_not_rejected() {
|
|
let c = Canvas::from_str(
|
|
"{\"nodes\": [{\"id\": \"a\", \"type\": \"text\", \"text\": \"t\", \"x\": 0, \"y\": 0, \"width\": 1, \"height\": 1, \"color\": \"7\"}]}",
|
|
)
|
|
.expect("tolerant read");
|
|
assert_eq!(c.nodes.first().map(|n| n.color.clone()), Some(Some(Color::Hex("7".into()))));
|
|
assert!(matches!(c.validate().first(), Some(Diagnostic::BadColor { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn huge_numbers_are_diagnosed_or_rejected() {
|
|
// serde_json rejects numbers that overflow f64.
|
|
assert!(matches!(err("{\"nodes\": [{\"id\": \"a\", \"type\": \"text\", \"x\": 1e400, \"y\": 0, \"width\": 1, \"height\": 1}]}"), Error::Json(_)));
|
|
// A finite but absurd coordinate is flagged, never rewritten.
|
|
let c = Canvas::from_str("{\"nodes\": [{\"id\": \"a\", \"type\": \"text\", \"text\": \"t\", \"x\": 1e308, \"y\": -1e308, \"width\": 1, \"height\": 1}]}")
|
|
.expect("parses");
|
|
let diags = c.validate();
|
|
assert_eq!(diags.len(), 2, "{diags:?}");
|
|
assert!(diags.iter().all(|d| matches!(d, Diagnostic::AbsurdCoordinate { .. })));
|
|
let again = Canvas::from_str(&c.to_string_pretty()).expect("re-parses");
|
|
assert_eq!(c, again, "absurd coordinates are not rewritten");
|
|
}
|
|
|
|
#[test]
|
|
fn deep_nesting_is_rejected_not_overflowed() {
|
|
let deep = format!("{}{}", "[".repeat(500), "]".repeat(500));
|
|
assert!(matches!(err(&deep), Error::Json(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn node_cap_is_enforced() {
|
|
let ok = gen_nodes(MAX_NODES);
|
|
let c = Canvas::from_str(&ok).expect("cap size parses");
|
|
assert_eq!(c.nodes.len(), MAX_NODES);
|
|
|
|
let too_many = gen_nodes(MAX_NODES + 1);
|
|
assert!(matches!(err(&too_many), Error::TooManyNodes { .. }));
|
|
}
|
|
|
|
fn gen_nodes(count: usize) -> String {
|
|
let mut s = String::from("{\"nodes\":[");
|
|
for i in 0..count {
|
|
if i > 0 {
|
|
s.push(',');
|
|
}
|
|
s.push_str("{\"id\":\"n");
|
|
s.push_str(&i.to_string());
|
|
s.push_str("\",\"type\":\"text\",\"text\":\"\",\"x\":0,\"y\":0,\"width\":1,\"height\":1}");
|
|
}
|
|
s.push_str("]}");
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn size_cap_is_enforced() {
|
|
let padding = "a".repeat(MAX_BYTES + 16);
|
|
let big = format!("{{\"nodes\":[],\"edges\":[],\"x\":\"{padding}\"}}");
|
|
assert!(big.len() > MAX_BYTES);
|
|
assert!(matches!(err(&big), Error::TooLarge { .. }));
|
|
}
|