use std::collections::BTreeMap; use wip_interface::WipType; use wip_site_worldspace::wip_protocol::{ CallOperationRequest, FetchInterfaceRequest, InterfaceReference, InterfaceTarget, ObserveRequest, ProtocolErrorCode, Target, Value, }; use wip_site_worldspace::{ DocumentContent, DocumentNode, MAX_DEPTH, MAX_NODES, Worldspace, WorldspaceData, }; fn node(path: &str, parent: Option<&str>, display: &str, href: &str) -> DocumentNode { DocumentNode { path: path.into(), name: if path == "/" { "" } else { path.rsplit('/').next().unwrap() } .into(), display_path: display.into(), parent: parent.map(str::to_owned), title: format!("Title {display}"), href: href.into(), html: format!("

{display}

"), } } fn data() -> WorldspaceData { WorldspaceData { nodes: vec![ node("/", None, "/", "/"), node("/book", Some("/"), "/book", "/book/"), node( "/book/1-chapter.md", Some("/book"), "/1-chapter.md", "/book/1-chapter/", ), node( "/book/1-chapter.md/1.1-section.md", Some("/book/1-chapter.md"), "/1.1-section.md", "/book/1.1-section/", ), // This intentional order differs from lexical order. node( "/book/10-last.md", Some("/book"), "/10-last.md", "/book/10-last/", ), node( "/book/2-second.md", Some("/book"), "/2-second.md", "/book/2-second/", ), node("/sources", Some("/"), "/sources", "/sources/"), node("/packages", Some("/"), "/packages", "/packages/"), ], } } fn read(path: &str) -> CallOperationRequest { CallOperationRequest { target: Target { path: path.into(), validator: None, }, interface: InterfaceTarget { reference: Worldspace::document_interface(), scope_ref: None, validator: None, }, operation: "read".into(), arguments: BTreeMap::new(), } } #[test] fn all_three_interactions_use_the_published_document_interface() { let world = Worldspace::new(data()).unwrap(); let request = ObserveRequest { path: "/book/1-chapter.md".into(), depth: 1, }; let observation = world.observe(request.clone()).unwrap(); request.validate_response(&observation).unwrap(); assert_eq!( observation.object.interfaces, vec![Worldspace::document_interface()] ); assert_eq!( observation.children.unwrap()[0].object.name, "1.1-section.md" ); let fetch = FetchInterfaceRequest { interface: Worldspace::document_interface(), }; let interface = world.fetch_interface(fetch.clone()).unwrap(); interface.validate_for(&fetch).unwrap(); assert_eq!(interface.descriptor.operations.len(), 1); assert_eq!(interface.descriptor.operations[0].name, "read"); assert!(interface.descriptor.operations[0].parameters.is_empty()); assert!(interface.scope_ref.is_none()); for expected in data().nodes { let call = read(&expected.path); let response = world.call_operation(call.clone()).unwrap(); response .validate_for(&interface.descriptor, &call.operation) .unwrap(); assert_eq!( DocumentContent::decode(response.result).unwrap(), DocumentContent { path: expected.path, title: expected.title, href: expected.href, html: expected.html, } ); } } #[test] fn depth_boundaries_empty_children_and_order_are_protocol_observations() { let world = Worldspace::new(data()).unwrap(); let observe = |path: &str, depth| { world .observe(ObserveRequest { path: path.into(), depth, }) .unwrap() }; assert!(observe("/", 0).children.is_none()); let root = observe("/", 1); let children = root.children.unwrap(); assert_eq!(children.len(), 3); assert_eq!(children[0].object.name, "book"); assert_eq!(children[1].object.name, "sources"); assert_eq!(children[2].object.name, "packages"); assert!(children.iter().all(|child| child.children.is_none())); for path in ["/sources", "/packages"] { assert!(observe(path, 0).children.is_none()); assert_eq!(observe(path, 1).children, Some(vec![])); let leaf = observe(path, 1); assert_eq!( leaf.object.interfaces, vec![Worldspace::document_interface()] ); let content = DocumentContent::decode(world.call_operation(read(path)).unwrap().result).unwrap(); assert_eq!(content.path, path); assert_eq!(content.href, format!("{path}/")); assert_eq!(content.html, format!("

{path}

")); } let root = observe("/", 3); let book = &root.children.as_ref().unwrap()[0]; let chapters = book.children.as_ref().unwrap(); assert_eq!( chapters .iter() .map(|node| node.object.name.as_str()) .collect::>(), vec!["1-chapter.md", "10-last.md", "2-second.md"] ); assert!(chapters[0].children.as_ref().unwrap()[0].children.is_none()); assert_eq!(chapters[1].children, Some(vec![])); assert_eq!( observe("/book/1-chapter.md/1.1-section.md", 1).children, Some(vec![]) ); assert!(observe("/", MAX_DEPTH).children.is_some()); assert_eq!( world .observe(ObserveRequest { path: "/".into(), depth: MAX_DEPTH + 1 }) .unwrap_err() .code, ProtocolErrorCode::ResourceLimitExceeded ); } #[test] fn canonical_host_failures_are_not_replaced_by_direct_body_lookup() { let world = Worldspace::new(data()).unwrap(); for path in ["relative", "/book/", "/book//x", "/book/../x", "/./book"] { assert_eq!( world .observe(ObserveRequest { path: path.into(), depth: 0 }) .unwrap_err() .code, ProtocolErrorCode::InvalidRequest ); assert_eq!( world.call_operation(read(path)).unwrap_err().code, ProtocolErrorCode::InvalidRequest ); } for path in [ "/missing", "/1-chapter.md", "/draft/private.md", "/book/draft/private.md", "/sources/repository", "/packages/crate", ] { assert_eq!( world .observe(ObserveRequest { path: path.into(), depth: 1 }) .unwrap_err() .code, ProtocolErrorCode::NotFound ); assert_eq!( world.call_operation(read(path)).unwrap_err().code, ProtocolErrorCode::NotFound ); } for reference in [ InterfaceReference { scope: "/".into(), name: "missing".into(), }, InterfaceReference { scope: "/book".into(), name: "document".into(), }, InterfaceReference { scope: "/draft".into(), name: "document".into(), }, ] { assert_eq!( world .fetch_interface(FetchInterfaceRequest { interface: reference.clone() }) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); let mut call = read("/book"); call.interface.reference = reference; assert_eq!( world.call_operation(call).unwrap_err().code, ProtocolErrorCode::InterfaceMismatch ); } assert_eq!( world .fetch_interface(FetchInterfaceRequest { interface: InterfaceReference { scope: "relative".into(), name: "document".into() }, }) .unwrap_err() .code, ProtocolErrorCode::InvalidRequest ); let mut call = read("/book"); call.interface.scope_ref = Some("not-the-published-root".into()); assert_eq!( world.call_operation(call).unwrap_err().code, ProtocolErrorCode::InterfaceMismatch ); let mut call = read("/book"); call.operation = "missing".into(); assert_eq!( world.call_operation(call).unwrap_err().code, ProtocolErrorCode::OperationNotFound ); let mut call = read("/book"); call.arguments .insert("extra".into(), Value::String("not accepted".into())); assert_eq!( world.call_operation(call).unwrap_err().code, ProtocolErrorCode::InvalidArguments ); let mut call = read("/book"); call.target.validator = Some(vec![1]); assert_eq!( world.call_operation(call).unwrap_err().code, ProtocolErrorCode::ValidatorMismatch ); let mut call = read("/book"); call.interface.validator = Some(vec![1]); assert_eq!( world.call_operation(call).unwrap_err().code, ProtocolErrorCode::InterfaceValidatorMismatch ); } #[test] fn rejects_duplicate_invalid_disconnected_and_private_input() { let reject = |mutate: fn(&mut WorldspaceData)| { let mut input = data(); mutate(&mut input); assert!(Worldspace::new(input).is_err()); }; reject(|data| data.nodes.push(data.nodes[0].clone())); reject(|data| data.nodes[2].display_path = "/book".into()); reject(|data| data.nodes[2].href = "/book/".into()); reject(|data| data.nodes[2].name = "a/b".into()); reject(|data| data.nodes[2].name = "..".into()); reject(|data| data.nodes[2].name.clear()); reject(|data| data.nodes[2].path = "/book//x".into()); reject(|data| data.nodes[2].path = "/elsewhere".into()); reject(|data| data.nodes[2].parent = None); reject(|data| data.nodes[3].parent = Some("/missing".into())); reject(|data| data.nodes[0].name = "root".into()); reject(|data| data.nodes[0].parent = Some("/".into())); reject(|data| data.nodes[0].display_path = "/home".into()); reject(|data| { data.nodes.remove(0); }); reject(|data| data.nodes[2].display_path = "/draft/secret.md".into()); reject(|data| { data.nodes .push(node("/draft", Some("/"), "/private", "/private/")) }); reject(|data| data.nodes[0].href = "//external.example/".into()); reject(|data| data.nodes[0].title.clear()); assert!(Worldspace::new(WorldspaceData { nodes: vec![] }).is_err()); assert!( Worldspace::new(WorldspaceData { nodes: vec![data().nodes[0].clone(); MAX_NODES + 1] }) .is_err() ); let mut wide = WorldspaceData { nodes: vec![data().nodes[0].clone()], }; for index in 1..MAX_NODES { let path = format!("/node-{index}"); wide.nodes .push(node(&path, Some("/"), &path, &format!("{path}/"))); } let world = Worldspace::new(wide).unwrap(); assert_eq!( world .observe(ObserveRequest { path: "/".into(), depth: 1 }) .unwrap() .children .unwrap() .len(), MAX_NODES - 1 ); let mut deep = WorldspaceData { nodes: vec![data().nodes[0].clone()], }; let mut parent = "/".to_string(); for _ in 0..=MAX_DEPTH { let path = format!("{}/a", parent.trim_end_matches('/')); deep.nodes .push(node(&path, Some(&parent), &path, &format!("{path}/"))); parent = path; if deep.nodes.len() == MAX_DEPTH as usize + 1 { assert!(Worldspace::new(deep.clone()).is_ok()); } } assert!(Worldspace::new(deep).is_err()); } #[test] fn json_contract_roundtrips_and_rejects_malformed_or_unknown_fields() { let json = serde_json::to_string(&data()).unwrap(); assert!(json.contains("displayPath")); let world = Worldspace::from_json(&json).unwrap(); assert!(world.call_operation(read("/")).is_ok()); assert!(Worldspace::from_json("not json").is_err()); assert!(Worldspace::from_json(r#"{"nodes":[],"draft":true}"#).is_err()); let too_large = " ".repeat(wip_site_worldspace::MAX_JSON_BYTES + 1); assert!(Worldspace::from_json(&too_large).is_err()); // Input order need not be topological; child order still follows the input. let mut shuffled = data(); shuffled.nodes.reverse(); assert!( Worldspace::new(shuffled) .unwrap() .observe(ObserveRequest { path: "/".into(), depth: 4 }) .is_ok() ); } /// This is deliberately explicit, not silently skipped when generated input is absent. /// Run after `npm run build`: cargo test -p wip-site-worldspace --test worldspace -- --include-ignored #[test] #[ignore = "requires the site's generated dist/worldspace.json (npm run build)"] fn generated_real_public_documents_support_all_interactions() { let json = std::fs::read_to_string(concat!( env!("CARGO_MANIFEST_DIR"), "/../dist/worldspace.json" )) .unwrap(); let data: WorldspaceData = serde_json::from_str(&json).unwrap(); assert!(data.nodes.len() > 10); let world = Worldspace::from_json(&json).unwrap(); let full = world .observe(ObserveRequest { path: "/".into(), depth: MAX_DEPTH, }) .unwrap(); assert_eq!( full.children .unwrap() .iter() .map(|node| node.object.name.as_str()) .collect::>(), vec!["book", "sources", "packages", "playground"] ); for path in ["/sources", "/packages"] { assert_eq!( world .observe(ObserveRequest { path: path.into(), depth: 1, }) .unwrap() .children, Some(vec![]) ); } let descriptor = world .fetch_interface(FetchInterfaceRequest { interface: Worldspace::document_interface(), }) .unwrap() .descriptor; for node in &data.nodes { assert!(!node.display_path.starts_with("/draft/")); assert!(!node.html.is_empty()); let request = read(&node.path); let response = world.call_operation(request.clone()).unwrap(); response .validate_for(&descriptor, &request.operation) .unwrap(); let content = DocumentContent::decode(response.result).unwrap(); assert_eq!(content.path, node.path); assert_eq!(content.href, node.href); assert_eq!(content.html, node.html); if let Some(parent) = &node.parent { assert_eq!( node.path, format!("{}/{}", parent.trim_end_matches('/'), node.name) ); } } assert!(data.nodes.iter().any(|node| node.path == "/book/1-concept.md/1.1-interaction-abstraction.md" && node.display_path == "/1.1-interaction-abstraction.md" && node.href.ends_with("/book/1.1-interaction-abstraction/"))); for path in ["/draft", "/book/draft", "/book/draft/6-security.md"] { assert_eq!( world.call_operation(read(path)).unwrap_err().code, ProtocolErrorCode::NotFound ); } }