//! External-crate contract tests: no Linux backend, private modules, or listener. use std::collections::BTreeMap; use std::sync::{Arc, Barrier, Mutex, TryLockError}; use std::time::{Duration, Instant}; use wip_fs_server::{ BackendEntry, Composition, DefaultInterfaces, EntryKind, FileRead, FilesystemBackend, HttpAdapter, Projection, ProjectionPolicy, ResolvedEntry, ResourceLimits, }; use wip_host::{ CancellationToken, EffectGuarantee, ExecutionContext, HostError, InterfaceRef, InterfaceRegistry, ObjectProvider, OperationAttempt, ProviderResult, PublishedObject, RegisteredInterface, Scope, Validator, WipObject, }; use wip_http::{ DecodedResponse, Endpoint, decode_call_operation_response, decode_fetch_interface_response, decode_observe_response, encode_call_operation_request, encode_fetch_interface_request, encode_observe_request, }; use wip_interface as wip; use wip_protocol::{ CallOperationRequest, CallOperationResponse, FetchInterfaceRequest, FetchInterfaceResponse, InterfaceDescriptor, InterfaceTarget, ObjectObservation, ObserveRequest, ProtocolErrorCode, Target, Value, }; const BACKEND_SECRET: &str = "/private/backend/credentials.key"; const POLICY_SECRET: &str = "policy-secret-tenant-42"; /// Application-defined capabilities, independent of built-in filesystem traits. #[wip::interface] pub trait MemoryOperations { /// Read through the application capability. /// # Returns /// Current bytes. fn read(&self) -> wip::OperationResult; /// Change the pinned target. /// # Returns /// Unit after changing the data. fn change( &self, /// New data. contents: wip::Bytes, ) -> wip::OperationResult<()>; /// A principal-hidden operation. /// # Returns /// Unit. fn hidden(&self) -> wip::OperationResult<()>; /// A visible but principal-denied operation. /// # Returns /// Unit. fn denied(&self) -> wip::OperationResult<()>; /// Inject a typed backend failure with an explicit effect guarantee. /// # Returns /// Unit on success (this test implementation always fails). fn fail( &self, /// Failure category and effect disposition. mode: u64, ) -> wip::OperationResult<()>; } #[derive(Clone, Debug, PartialEq, Eq)] struct Data { bytes: Vec, revision: u64, hidden: bool, } struct Node { id: u64, kind: EntryKind, data: Mutex, calls: Mutex>, } #[derive(Clone)] struct Entry { path: String, node: Arc, validator: Validator, hidden: bool, } impl Entry { fn snapshot(path: String, node: Arc) -> Self { let data = node.data.lock().unwrap(); let revision = data.revision; let hidden = data.hidden; drop(data); let validator = Validator::new(format!("{}:{revision}", node.id).into_bytes()); Self { path, node, validator, hidden, } } } impl WipObject for Entry { fn name(&self) -> &str { self.path.rsplit('/').next().unwrap() } fn description(&self) -> Option<&str> { Some("public memory entry") } fn reference(&self) -> Option<&str> { (self.path == "/").then_some("entry-root") } fn validator(&self) -> Option<&Validator> { Some(&self.validator) } } impl BackendEntry for Entry { fn kind(&self) -> EntryKind { self.node.kind } } #[derive(Debug)] struct Seen { stage: &'static str, path: String, deadline: Option, budget: Option, token: CancellationToken, } #[derive(Default)] struct Controls { bump_before_execute: bool, bump_ancestor: Option, hide_before_execute: bool, hide_ancestor: Option, replace_ancestor: Option, replace_mapping: bool, rollback: bool, cancel_on_execute: bool, cancel_on_publish: bool, resolve_error: bool, children_error: bool, publish_error: bool, execute_error: Option, } struct Shared { nodes: Mutex>>, // Participating memory writers serialize validation, dispatch and disposition. // Deliberate out-of-band mutation below simulates the documented writer limit. transaction: Mutex<()>, controls: Mutex, seen: Mutex>, } impl Shared { fn record(&self, stage: &'static str, path: &str, context: &ExecutionContext) { self.seen.lock().unwrap().push(Seen { stage, path: path.into(), deadline: context.deadline(), budget: context.resource_budget(), token: context.cancellation().clone(), }); } fn node(&self, path: &str) -> Arc { self.nodes.lock().unwrap()[path].clone() } } #[derive(Clone)] struct MemoryBackend(Arc, Arc); impl MemoryBackend { fn new(shared: Arc) -> Self { let root = shared.node("/"); Self(shared, root) } } fn check_context(context: &ExecutionContext) -> Result<(), HostError> { if context.cancellation().is_cancelled() || context .deadline() .is_some_and(|deadline| Instant::now() >= deadline) || context.resource_budget() == Some(0) { Err(HostError::resource_limit( BACKEND_SECRET, EffectGuarantee::NotCommitted, )) } else { Ok(()) } } impl FilesystemBackend for MemoryBackend { type Entry = Entry; fn resolve( &self, context: &ExecutionContext, path: &str, ) -> ProviderResult> { self.0.record("resolve", path, context); if let Err(error) = check_context(context) { return ProviderResult::Failure(error); } if self.0.controls.lock().unwrap().resolve_error { return ProviderResult::Failure(HostError::internal( BACKEND_SECRET, EffectGuarantee::NotCommitted, )); } // One connected map snapshot, serialized with participating mutations. let _transaction = self.0.transaction.lock().unwrap(); let nodes = self.0.nodes.lock().unwrap(); if !nodes .get("/") .is_some_and(|root| Arc::ptr_eq(root, &self.1)) { return ProviderResult::NotFound; } let Some(node) = nodes.get(path) else { return ProviderResult::NotFound; }; let mut ancestors = Vec::new(); if path != "/" { let prefixes = std::iter::once("/").chain( path.match_indices('/') .skip(1) .map(|(index, _)| &path[..index]), ); for prefix in prefixes { let Some(parent) = nodes.get(prefix) else { return ProviderResult::NotFound; }; if parent.kind != EntryKind::Directory { return ProviderResult::NotFound; } ancestors.push(Entry::snapshot(prefix.into(), parent.clone())); } } ProviderResult::Found(ResolvedEntry::new( ancestors, Entry::snapshot(path.into(), node.clone()), )) } fn children( &self, context: &ExecutionContext, entry: &Entry, maximum: usize, ) -> Result, HostError> { self.0.record("children", &entry.path, context); check_context(context)?; if self.0.controls.lock().unwrap().children_error { return Err(HostError::internal( BACKEND_SECRET, EffectGuarantee::NotCommitted, )); } let prefix = if entry.path == "/" { "/".into() } else { format!("{}/", entry.path) }; let nodes = self.0.nodes.lock().unwrap(); let children: Vec<_> = nodes .iter() .filter(|(path, _)| { path.strip_prefix(&prefix) .is_some_and(|rest| !rest.is_empty() && !rest.contains('/')) }) .map(|(path, node)| Entry::snapshot(path.clone(), node.clone())) .collect(); if children.len() > maximum { return Err(HostError::resource_limit( BACKEND_SECRET, EffectGuarantee::NotCommitted, )); } Ok(children) } fn execute( &self, context: &ExecutionContext, resolved: ResolvedEntry, operation: impl FnOnce(ResolvedEntry) -> OperationAttempt, ) -> ProviderResult { let _lock = self.0.transaction.lock().unwrap(); let (ancestors, entry) = resolved.into_parts(); self.0.record("execute", &entry.path, context); if let Err(error) = check_context(context) { return ProviderResult::Failure(error); } let mut controls = self.0.controls.lock().unwrap(); if let Some(guarantee) = controls.execute_error { return ProviderResult::Failure(HostError::internal(BACKEND_SECRET, guarantee)); } if controls.bump_before_execute { entry.node.data.lock().unwrap().revision += 1; controls.bump_before_execute = false; } if let Some(path) = controls.bump_ancestor.take() { self.0.node(&path).data.lock().unwrap().revision += 1; } if controls.hide_before_execute { entry.node.data.lock().unwrap().hidden = true; } if let Some(path) = controls.hide_ancestor.take() { self.0.node(&path).data.lock().unwrap().hidden = true; } if let Some(path) = controls.replace_ancestor.take() { self.0.nodes.lock().unwrap().insert( path, Arc::new(Node { id: 1000, kind: EntryKind::Directory, data: Mutex::new(Data { bytes: b"replacement ancestor".to_vec(), revision: 1, hidden: false, }), calls: Mutex::new(Vec::new()), }), ); } if controls.replace_mapping { // The callback must still receive the original capability, not this decoy. self.0.nodes.lock().unwrap().insert( entry.path.clone(), Arc::new(Node { id: 999, kind: EntryKind::Regular, data: Mutex::new(Data { bytes: b"decoy".to_vec(), revision: 1, hidden: false, }), calls: Mutex::new(vec![]), }), ); controls.replace_mapping = false; } if controls.cancel_on_execute { context.cancellation().cancel(); } let rollback = controls.rollback; drop(controls); // Verify lineage identity/membership within the same transaction. The // callback cannot see a substituted ancestor, nor a redirected target. let nodes = self.0.nodes.lock().unwrap(); if !nodes .get("/") .is_some_and(|root| Arc::ptr_eq(root, &self.1)) { return ProviderResult::NotFound; } for ancestor in &ancestors { if !nodes .get(&ancestor.path) .is_some_and(|node| Arc::ptr_eq(node, &ancestor.node)) { return ProviderResult::NotFound; } } drop(nodes); let refreshed_ancestors = ancestors .into_iter() .map(|ancestor| Entry::snapshot(ancestor.path, ancestor.node)) .collect(); let before = entry.node.data.lock().unwrap().clone(); let refreshed = Entry::snapshot(entry.path.clone(), entry.node.clone()); let attempt = operation(ResolvedEntry::new(refreshed_ancestors, refreshed)); let attempt = match attempt { OperationAttempt::Started(Ok(mut result)) => { result.validator = Some( Entry::snapshot(entry.path, entry.node.clone()) .validator .as_bytes() .to_vec(), ); OperationAttempt::Started(Ok(result)) } failed @ OperationAttempt::Started(Err(_)) if rollback => { *entry.node.data.lock().unwrap() = before; // The guarantee is upgraded only after the data is restored. failed.confirm_rollback() } other => other, }; ProviderResult::Found(attempt) } } // Local wrapper avoids orphan rules and implements both external and local traits. struct Object { entry: Entry, custom: InterfaceRef, shared: Arc, context: ExecutionContext, } impl WipObject for Object { fn name(&self) -> &str { self.entry.name() } fn description(&self) -> Option<&str> { self.entry.description() } fn reference(&self) -> Option<&str> { // Public representation identity need not be the backend entry ref. // ScopeObservation must capture this published Object's ref. (self.entry.path == "/").then_some("memory-root") } fn validator(&self) -> Option<&Validator> { self.entry.validator() } fn requires_validator(&self, interface: &InterfaceRef, operation: &str) -> bool { interface == &self.custom && matches!(operation, "read" | "change") } } impl Object { fn invoked(&self, operation: &'static str) { assert!( matches!( self.shared.transaction.try_lock(), Err(TryLockError::WouldBlock) ), "typed dispatch escaped the backend lock" ); self.shared.record("typed", &self.entry.path, &self.context); self.entry.node.calls.lock().unwrap().push(operation); } fn change_data(&self, bytes: Vec) { let mut data = self.entry.node.data.lock().unwrap(); data.bytes = bytes; data.revision += 1; } } impl MemoryOperations for Object { fn read(&self) -> wip::OperationResult { self.invoked("custom.read"); Ok(wip::Bytes( self.entry.node.data.lock().unwrap().bytes.clone(), )) } fn change(&self, contents: wip::Bytes) -> wip::OperationResult<()> { self.invoked("custom.change"); self.change_data(contents.0); Ok(()) } fn hidden(&self) -> wip::OperationResult<()> { self.invoked("custom.hidden"); Ok(()) } fn denied(&self) -> wip::OperationResult<()> { self.invoked("custom.denied"); Ok(()) } fn fail(&self, mode: u64) -> wip::OperationResult<()> { self.invoked("custom.fail"); let guarantee = if mode < 2 { EffectGuarantee::NotCommitted } else { self.change_data(b"uncertain effects".to_vec()); EffectGuarantee::Unknown }; let diagnostic = format!("{BACKEND_SECRET}: {POLICY_SECRET}"); Err(if mode.is_multiple_of(2) { HostError::internal(diagnostic, guarantee) } else { HostError::resource_limit(diagnostic, guarantee) }) } } impl FileRead for Object { fn read(&self, offset: Option, length: Option) -> wip::OperationResult { self.invoked("default.read"); let data = self.entry.node.data.lock().unwrap(); let start = usize::try_from(offset.unwrap_or(0)) .unwrap_or(usize::MAX) .min(data.bytes.len()); let count = usize::try_from(length.unwrap_or(u64::MAX)).unwrap_or(usize::MAX); Ok(wip::Bytes( data.bytes[start..start.saturating_add(count).min(data.bytes.len())].to_vec(), )) } } struct MemoryComposition { defaults: DefaultInterfaces, custom: RegisteredInterface, shared: Arc, } impl Composition for MemoryComposition { type Object = Object; fn publish( &self, entry: Entry, context: &ExecutionContext, ) -> Result, HostError> { self.shared.record("publish", &entry.path, context); let controls = self.shared.controls.lock().unwrap(); if controls.publish_error { return Err(HostError::internal( format!("{BACKEND_SECRET}: {POLICY_SECRET}"), EffectGuarantee::NotCommitted, )); } if controls.cancel_on_publish { context.cancellation().cancel(); } drop(controls); let path = entry.path.clone(); let kind = entry.kind(); let mut publication = PublishedObject::new(Object { entry, custom: self.custom.reference().clone(), shared: self.shared.clone(), context: context.clone(), }); // Entry conditions add, replace, or hide the default capability. if kind == EntryKind::Regular && !matches!(path.as_str(), "/replacement" | "/metadata") { publication = publication.expose(&self.defaults.read).unwrap(); } if matches!( path.as_str(), "/added" | "/replacement" | "/secret/nested/data" ) { publication = publication.expose(&self.custom).unwrap(); } Ok(publication) } } #[derive(Clone, Copy, Debug)] enum Principal { Admin, Limited, } struct Policy { custom: InterfaceRef, read: InterfaceRef, hide_read: bool, invalid_selection: bool, panic_authorize: bool, } impl ProjectionPolicy for Policy { type Principal = Principal; fn object_visible(&self, principal: &Principal, entry: &Entry) -> bool { !entry.hidden && (!matches!(principal, Principal::Limited) || entry.path != "/secret") } fn interface_visible(&self, principal: &Principal, interface: &InterfaceRef) -> bool { !(matches!(principal, Principal::Limited) && self.hide_read && interface == &self.read) } fn interfaces( &self, principal: &Principal, entry: &Entry, available: &[InterfaceRef], ) -> Vec { if self.invalid_selection && entry.path == "/plain" { return vec![self.custom.clone()]; } let mut selected = available.to_vec(); if matches!(principal, Principal::Limited) { selected.reverse(); } if matches!(principal, Principal::Limited) && entry.path == "/policy-metadata" { selected.clear(); } selected } fn operation_visible( &self, principal: &Principal, interface: &InterfaceRef, operation: &str, ) -> bool { !(matches!(principal, Principal::Limited) && interface == &self.custom && operation == "hidden") } fn authorize( &self, principal: &Principal, _: &Object, interface: &InterfaceRef, operation: &str, ) -> bool { if self.panic_authorize { panic!("{POLICY_SECRET}"); } !(matches!(principal, Principal::Limited) && interface == &self.custom && operation == "denied") } } #[test] fn observation_and_fetch_policy_panics_remain_sanitized_read_failures() { struct PanickingPolicy(bool); impl ProjectionPolicy for PanickingPolicy { type Principal = (); fn object_visible(&self, _: &(), _: &Entry) -> bool { if self.0 { panic!("{POLICY_SECRET}"); } true } fn interface_visible(&self, _: &(), _: &InterfaceRef) -> bool { if !self.0 { panic!("{POLICY_SECRET}"); } true } } for panic_object in [true, false] { let fixture = Fixture::new(); let mut registry = InterfaceRegistry::new(); let scope = Scope::new("/").unwrap(); let defaults = DefaultInterfaces::register(&mut registry, &scope).unwrap(); let custom = registry .register( &scope, "memory.operations", MemoryOperationsDefinition, None, ) .unwrap(); let reference = custom.reference().protocol_reference(); let engine = Projection::new( MemoryBackend::new(fixture.shared.clone()), registry, MemoryComposition { defaults, custom, shared: fixture.shared.clone(), }, PanickingPolicy(panic_object), fixture.limits, ); let view = engine.view(&()); let error = view .observe( &ExecutionContext::new(), &ObserveRequest { path: "/added".into(), depth: 0, }, ) .unwrap_err(); assert_eq!(error.code, ProtocolErrorCode::Internal); assert!(!error.message.contains(POLICY_SECRET)); if !panic_object { let error = view .fetch_interface(&FetchInterfaceRequest { interface: reference, }) .unwrap_err(); assert_eq!(error.code, ProtocolErrorCode::Internal); assert!(!error.message.contains(POLICY_SECRET)); } } } type Engine = Projection; struct Fixture { engine: Engine, shared: Arc, custom: wip_protocol::InterfaceReference, read: wip_protocol::InterfaceReference, limits: ResourceLimits, // Client-known descriptors acquired coherently before execution fault injection. descriptors: BTreeMap<(bool, wip_protocol::InterfaceReference), InterfaceDescriptor>, } impl Fixture { fn new() -> Self { Self::configured(false, false, false, None, ResourceLimits::default()) } fn configured( hide_read: bool, invalid_selection: bool, panic_authorize: bool, interface_validator: Option, limits: ResourceLimits, ) -> Self { let mut nodes = BTreeMap::new(); for (index, (path, kind)) in [ ("/", EntryKind::Directory), ("/plain", EntryKind::Regular), ("/added", EntryKind::Regular), ("/replacement", EntryKind::Regular), ("/metadata", EntryKind::Regular), ("/policy-metadata", EntryKind::Regular), ("/unsupported", EntryKind::Unsupported), ("/link", EntryKind::Symlink), ("/secret", EntryKind::Directory), ("/secret/nested", EntryKind::Directory), ("/secret/nested/data", EntryKind::Regular), ] .into_iter() .enumerate() { nodes.insert( path.into(), Arc::new(Node { id: index as u64, kind, data: Mutex::new(Data { bytes: b"hello memory".to_vec(), revision: 1, hidden: false, }), calls: Mutex::new(vec![]), }), ); } let shared = Arc::new(Shared { nodes: Mutex::new(nodes), transaction: Mutex::new(()), controls: Mutex::new(Controls::default()), seen: Mutex::new(vec![]), }); let mut registry = InterfaceRegistry::new(); let scope = Scope::new("/").unwrap(); let defaults = DefaultInterfaces::register(&mut registry, &scope).unwrap(); let custom = registry .register( &scope, "memory.operations", MemoryOperationsDefinition, interface_validator, ) .unwrap(); let read_ref = defaults.read.reference().clone(); let custom_ref = custom.reference().clone(); let policy = Policy { custom: custom_ref.clone(), read: read_ref.clone(), hide_read, invalid_selection, panic_authorize, }; let composition = MemoryComposition { defaults, custom, shared: shared.clone(), }; let engine = Projection::new( MemoryBackend::new(shared.clone()), registry, composition, policy, limits, ); let mut descriptors = BTreeMap::new(); if !invalid_selection { for principal in [Principal::Admin, Principal::Limited] { for reference in [&read_ref, &custom_ref] { let reference = reference.protocol_reference(); if let Ok(observation) = engine .view(&principal) .fetch_interface(&fetch_request(&reference)) { descriptors.insert( (matches!(principal, Principal::Admin), reference), observation.descriptor, ); } } } } shared.seen.lock().unwrap().clear(); Self { engine, shared, custom: custom_ref.protocol_reference(), read: read_ref.protocol_reference(), limits, descriptors, } } fn observe(&self, principal: &Principal, path: &str, depth: u32) -> ObjectObservation { self.engine .view(principal) .observe(&ExecutionContext::new(), &observe_request(path, depth)) .unwrap() } fn descriptor( &self, principal: &Principal, reference: &wip_protocol::InterfaceReference, ) -> InterfaceDescriptor { self.descriptors[&(matches!(principal, Principal::Admin), reference.clone())].clone() } fn request( &self, path: &str, reference: &wip_protocol::InterfaceReference, operation: &str, validator: Option>, ) -> CallOperationRequest { CallOperationRequest { target: Target { path: path.into(), validator, }, interface: InterfaceTarget { scope_ref: None, reference: reference.clone(), validator: None, }, operation: operation.into(), arguments: BTreeMap::new(), } } fn http_call( &self, principal: &Principal, request: &CallOperationRequest, descriptor: &InterfaceDescriptor, context: &ExecutionContext, ) -> ( http::Response>, DecodedResponse, ) { let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let view = self.engine.view(principal); let response = HttpAdapter::new(&view, &endpoint, self.limits).handle_http_with_context( encode_call_operation_request(&endpoint, request, descriptor, self.limits.wire) .unwrap(), context, ); let decoded = decode_call_operation_response(request, descriptor, &response, self.limits.wire) .unwrap(); (response, decoded) } } #[test] fn ancestor_visibility_refresh_inside_boundary_prevents_descendant_invocation_and_effects() { for (target, ancestor) in [ ("/added", "/"), ("/secret/nested/data", "/"), ("/secret/nested/data", "/secret"), ("/secret/nested/data", "/secret/nested"), ] { for operation in ["denied", "read", "change"] { let fixture = Fixture::new(); let observation = fixture.observe(&Principal::Admin, target, 0); let descriptor = fixture.descriptor(&Principal::Admin, &fixture.custom); let mut request = fixture.request( target, &fixture.custom, operation, observation.object.validator, ); if operation == "change" { request .arguments .insert("contents".into(), Value::Bytes(b"must not commit".to_vec())); } let leaf = fixture.shared.node(target); let before = leaf.data.lock().unwrap().clone(); fixture.shared.controls.lock().unwrap().hide_ancestor = Some(ancestor.into()); let (wire, response) = fixture.http_call( &Principal::Admin, &request, &descriptor, &ExecutionContext::new(), ); wire_error(response, ProtocolErrorCode::NotFound); assert!(leaf.calls.lock().unwrap().is_empty()); assert_eq!(*leaf.data.lock().unwrap(), before); assert!(!String::from_utf8_lossy(wire.body()).contains(BACKEND_SECRET)); let error = fixture .engine .view(&Principal::Admin) .observe(&ExecutionContext::new(), &observe_request(target, 0)) .unwrap_err(); assert_eq!(error.code, ProtocolErrorCode::NotFound); // The entire target resolves once, not as unrelated path prefixes. let seen = fixture.shared.seen.lock().unwrap(); assert!( seen.iter() .filter(|event| event.stage == "resolve") .all(|event| event.path == target) ); } } } #[test] fn replaced_visible_ancestors_reject_without_redirecting_or_dispatching_the_pinned_target() { for (target, ancestor) in [ ("/added", "/"), ("/secret/nested/data", "/secret"), ("/secret/nested/data", "/secret/nested"), ] { let fixture = Fixture::new(); let observation = fixture.observe(&Principal::Admin, target, 0); let descriptor = fixture.descriptor(&Principal::Admin, &fixture.custom); let mut request = fixture.request( target, &fixture.custom, "change", observation.object.validator, ); request .arguments .insert("contents".into(), Value::Bytes(b"must not commit".to_vec())); let leaf = fixture.shared.node(target); let original_ancestor = fixture.shared.node(ancestor); let before = leaf.data.lock().unwrap().clone(); fixture.shared.controls.lock().unwrap().replace_ancestor = Some(ancestor.into()); let (_, response) = fixture.http_call( &Principal::Admin, &request, &descriptor, &ExecutionContext::new(), ); wire_error(response, ProtocolErrorCode::NotFound); assert!(leaf.calls.lock().unwrap().is_empty()); assert_eq!(*leaf.data.lock().unwrap(), before); assert!(!Arc::ptr_eq( &original_ancestor, &fixture.shared.node(ancestor) )); assert!( fixture .shared .node(ancestor) .calls .lock() .unwrap() .is_empty() ); // A replacement is visible but cannot authorize a previously resolved lineage. assert!(!fixture.shared.node(ancestor).data.lock().unwrap().hidden); } } fn observe_request(path: &str, depth: u32) -> ObserveRequest { ObserveRequest { path: path.into(), depth, } } fn fetch_request(reference: &wip_protocol::InterfaceReference) -> FetchInterfaceRequest { FetchInterfaceRequest { interface: reference.clone(), } } fn success(response: DecodedResponse) -> T { match response { DecodedResponse::Success(value) => value, other => panic!("expected success, got {other:?}"), } } fn wire_error(response: DecodedResponse, expected: ProtocolErrorCode) { match response { DecodedResponse::ProtocolFailure { error, status_mismatch, } => { assert_eq!(error.code, expected); assert!(status_mismatch.is_none(), "noncanonical HTTP error status"); } other => panic!("expected {expected:?}, got {other:?}"), } } fn no_secrets(response: &http::Response>) { let body = std::str::from_utf8(response.body()).unwrap(); assert!( !body.contains(BACKEND_SECRET), "backend path leaked: {body}" ); assert!( !body.contains(POLICY_SECRET), "policy diagnostic leaked: {body}" ); assert!( response.body().len() < 1024, "failure diagnostic was not bounded" ); } #[test] fn external_composition_adds_replaces_and_hides_default_interfaces_by_entry_condition() { let fixture = Fixture::new(); assert!(Arc::ptr_eq(&fixture.engine.backend().0, &fixture.shared)); for (path, expected) in [ ("/plain", vec![fixture.read.clone()]), ("/added", vec![fixture.read.clone(), fixture.custom.clone()]), ("/replacement", vec![fixture.custom.clone()]), ("/metadata", vec![]), ("/unsupported", vec![]), ("/link", vec![]), ("/", vec![]), ] { let observation = fixture.observe(&Principal::Admin, path, 0); assert_eq!(observation.object.interfaces, expected, "{path}"); assert_eq!( observation.object.description.as_deref(), Some("public memory entry") ); assert!(observation.children.is_none()); assert_eq!( observation.object.r#ref.as_deref(), (path == "/").then_some("memory-root") ); for reference in &observation.object.interfaces { let fetched = fixture .engine .view(&Principal::Admin) .fetch_interface(&fetch_request(reference)) .unwrap(); assert_eq!(&fetched.interface, reference); fetched.descriptor.validate().unwrap(); } } let request = fixture.request("/added", &fixture.read, "read", None); assert_eq!( fixture .engine .view(&Principal::Admin) .call_operation(request) .unwrap() .result, Value::Bytes(b"hello memory".to_vec()) ); let observation = fixture.observe(&Principal::Admin, "/replacement", 0); let request = fixture.request( "/replacement", &fixture.custom, "read", observation.object.validator, ); assert_eq!( fixture .engine .view(&Principal::Admin) .call_operation(request) .unwrap() .result, Value::Bytes(b"hello memory".to_vec()) ); let request = fixture.request("/replacement", &fixture.read, "read", None); assert_eq!( fixture .engine .view(&Principal::Admin) .call_operation(request) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::InterfaceMismatch ); } #[test] fn principal_views_hide_children_and_guessed_descendants_for_observation_and_calls() { let fixture = Fixture::new(); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let context = ExecutionContext::new(); for principal in [Principal::Admin, Principal::Limited] { let view = fixture.engine.view(&principal); let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits); let request = observe_request("/", 3); let response = adapter.handle_http_with_context( encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(), &context, ); let tree = success(decode_observe_response(&request, &response, fixture.limits.wire).unwrap()); let secret = tree .children .unwrap() .into_iter() .find(|child| child.object.name == "secret"); match principal { Principal::Admin => assert_eq!( secret.unwrap().children.unwrap()[0] .children .as_ref() .unwrap()[0] .object .name, "data" ), Principal::Limited => assert!(secret.is_none()), } } fixture.shared.seen.lock().unwrap().clear(); for path in ["/secret", "/secret/nested", "/secret/nested/data"] { let view = fixture.engine.view(&Principal::Limited); assert_eq!( view.observe(&context, &observe_request(path, 0)) .unwrap_err() .code, ProtocolErrorCode::NotFound ); assert_eq!( view.observe_object(path).unwrap_err().protocol_error().code, ProtocolErrorCode::NotFound ); let request = fixture.request(path, &fixture.custom, "denied", None); let (response, decoded) = fixture.http_call( &Principal::Limited, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &context, ); wire_error(decoded, ProtocolErrorCode::NotFound); no_secrets(&response); } assert!( fixture .shared .seen .lock() .unwrap() .iter() .all(|seen| seen.stage == "resolve"), "hidden lineage was published, enumerated, or dispatched" ); assert!( fixture .shared .node("/secret/nested/data") .calls .lock() .unwrap() .is_empty() ); // A principal does not mutate the other principal's descriptor or tree. assert_eq!( fixture .observe(&Principal::Admin, "/secret/nested/data", 0) .object .name, "data" ); } #[test] fn projected_descriptor_and_dispatch_distinguish_hidden_missing_and_denied_operations() { let fixture = Fixture::new(); let admin = fixture.descriptor(&Principal::Admin, &fixture.custom); let limited = fixture.descriptor(&Principal::Limited, &fixture.custom); assert!( admin .operations .iter() .any(|operation| operation.name == "hidden") ); assert!( !limited .operations .iter() .any(|operation| operation.name == "hidden") ); assert!( limited .operations .iter() .any(|operation| operation.name == "denied") ); assert_eq!( fixture .observe(&Principal::Limited, "/added", 0) .object .interfaces, vec![fixture.custom.clone(), fixture.read.clone()] ); let view = fixture.engine.view(&Principal::Limited); for (operation, code) in [ ("hidden", ProtocolErrorCode::OperationNotFound), ("absent", ProtocolErrorCode::OperationNotFound), ("denied", ProtocolErrorCode::PermissionDenied), ] { let request = fixture.request("/added", &fixture.custom, operation, None); assert_eq!( view.call_operation(request.clone()) .unwrap_err() .protocol_error() .code, code ); if operation != "absent" { // Encode against the full descriptor to simulate a guessed hidden operation. let (response, decoded) = fixture.http_call( &Principal::Limited, &request, &admin, &ExecutionContext::new(), ); wire_error(decoded, code); no_secrets(&response); } } assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); let request = fixture.request("/added", &fixture.custom, "denied", None); fixture .engine .view(&Principal::Admin) .call_operation(request) .unwrap(); assert_eq!( *fixture.shared.node("/added").calls.lock().unwrap(), vec!["custom.denied"] ); assert_eq!( fixture.descriptor(&Principal::Admin, &fixture.custom), admin ); } #[test] fn interface_visibility_filters_publication_and_fetch_together() { let fixture = Fixture::configured(true, false, false, None, ResourceLimits::default()); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let view = fixture.engine.view(&Principal::Limited); let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits); assert_eq!( fixture .observe(&Principal::Limited, "/added", 0) .object .interfaces, vec![fixture.custom.clone()] ); assert!( fixture .observe(&Principal::Limited, "/plain", 0) .object .interfaces .is_empty() ); let tree = fixture.observe(&Principal::Limited, "/", 1); for child in tree.children.unwrap() { for reference in child.object.interfaces { let request = fetch_request(&reference); let response = adapter.handle_http_with_context( encode_fetch_interface_request(&endpoint, &request, fixture.limits.wire).unwrap(), &ExecutionContext::new(), ); let fetched: FetchInterfaceResponse = success( decode_fetch_interface_response(&request, &response, fixture.limits.wire).unwrap(), ); assert_eq!(fetched.interface, reference); } } let request = fetch_request(&fixture.read); let response = adapter.handle_http_with_context( encode_fetch_interface_request(&endpoint, &request, fixture.limits.wire).unwrap(), &ExecutionContext::new(), ); wire_error( decode_fetch_interface_response(&request, &response, fixture.limits.wire).unwrap(), ProtocolErrorCode::InterfaceNotFound, ); let request = fixture.request("/added", &fixture.read, "read", None); assert_eq!( view.call_operation(request) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::InterfaceMismatch ); assert_eq!( fixture .observe(&Principal::Admin, "/added", 0) .object .interfaces, vec![fixture.read.clone(), fixture.custom.clone()] ); assert!( fixture .engine .view(&Principal::Admin) .fetch_interface(&fetch_request(&fixture.read)) .is_ok() ); } #[test] fn entry_specific_policy_selects_a_subset_without_globally_hiding_the_descriptor() { let fixture = Fixture::new(); let admin = fixture.observe(&Principal::Admin, "/policy-metadata", 0); let limited = fixture.observe(&Principal::Limited, "/policy-metadata", 0); assert_eq!(admin.object.interfaces, vec![fixture.read.clone()]); assert!(limited.object.interfaces.is_empty()); assert_eq!(admin.object.validator, limited.object.validator); let request = fixture.request("/policy-metadata", &fixture.read, "read", None); let (response, decoded) = fixture.http_call( &Principal::Limited, &request, &fixture.descriptor(&Principal::Limited, &fixture.read), &ExecutionContext::new(), ); wire_error(decoded, ProtocolErrorCode::InterfaceMismatch); no_secrets(&response); assert!( fixture .shared .node("/policy-metadata") .calls .lock() .unwrap() .is_empty() ); fixture .engine .view(&Principal::Admin) .call_operation(request) .unwrap(); assert_eq!( *fixture .shared .node("/policy-metadata") .calls .lock() .unwrap(), vec!["default.read"] ); } #[test] fn http_observe_fetch_and_call_round_trip_uses_only_client_observed_publications() { for principal in [Principal::Admin, Principal::Limited] { let fixture = Fixture::new(); let context = ExecutionContext::new(); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let view = fixture.engine.view(&principal); let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits); let observe = observe_request("/added", 0); let response = adapter.handle_http_with_context( encode_observe_request(&endpoint, &observe, fixture.limits.wire).unwrap(), &context, ); let observed = success(decode_observe_response(&observe, &response, fixture.limits.wire).unwrap()) .object; let mut custom_descriptor = None; for reference in observed.interfaces { let fetch = fetch_request(&reference); let response = adapter.handle_http_with_context( encode_fetch_interface_request(&endpoint, &fetch, fixture.limits.wire).unwrap(), &context, ); let fetched = success( decode_fetch_interface_response(&fetch, &response, fixture.limits.wire).unwrap(), ); let application_interface = fetched .descriptor .operations .iter() .any(|operation| operation.name == "change"); let mut request = fixture.request( "/added", &fetched.interface, "read", if application_interface { observed.validator.clone() } else { None }, ); request.interface.validator = fetched.validator; if !application_interface { request.arguments.insert("offset".into(), Value::Integer(6)); request.arguments.insert("length".into(), Value::Integer(6)); } let response = adapter.handle_http_with_context( encode_call_operation_request( &endpoint, &request, &fetched.descriptor, fixture.limits.wire, ) .unwrap(), &context, ); let result = success( decode_call_operation_response( &request, &fetched.descriptor, &response, fixture.limits.wire, ) .unwrap(), ); assert_eq!( result.result, Value::Bytes(if application_interface { b"hello memory".to_vec() } else { b"memory".to_vec() }) ); assert_eq!(result.validator, observed.validator); if application_interface { assert_eq!( fetched .descriptor .operations .iter() .any(|operation| operation.name == "hidden"), matches!(principal, Principal::Admin) ); custom_descriptor = Some((fetched.interface, fetched.descriptor)); } } let (reference, descriptor) = custom_descriptor.unwrap(); let mut request = fixture.request("/added", &reference, "change", observed.validator.clone()); request .arguments .insert("contents".into(), Value::Bytes(b"HTTP commit".to_vec())); let response = adapter.handle_http_with_context( encode_call_operation_request(&endpoint, &request, &descriptor, fixture.limits.wire) .unwrap(), &context, ); let result = success( decode_call_operation_response(&request, &descriptor, &response, fixture.limits.wire) .unwrap(), ); assert_eq!(result.result, Value::Unit); assert_ne!(result.validator, observed.validator); let response = adapter.handle_http_with_context( encode_observe_request(&endpoint, &observe, fixture.limits.wire).unwrap(), &context, ); let committed = success(decode_observe_response(&observe, &response, fixture.limits.wire).unwrap()); assert_eq!(result.validator, committed.object.validator); assert_eq!( fixture.shared.node("/added").data.lock().unwrap().bytes, b"HTTP commit" ); } } #[test] fn policy_cannot_add_an_unimplemented_interface() { let fixture = Fixture::configured(false, true, false, None, ResourceLimits::default()); let view = fixture.engine.view(&Principal::Admin); assert_eq!( view.observe(&ExecutionContext::new(), &observe_request("/plain", 0)) .unwrap_err() .code, ProtocolErrorCode::Internal ); let request = fixture.request("/plain", &fixture.custom, "denied", None); // This deliberately invalid publication cannot supply a fetch response. // Use the application's authored schema solely to encode the test request. let (response, decoded) = fixture.http_call( &Principal::Admin, &request, &MemoryOperationsDefinition::descriptor(), &ExecutionContext::new(), ); wire_error(decoded, ProtocolErrorCode::Internal); no_secrets(&response); assert!( fixture .shared .node("/plain") .calls .lock() .unwrap() .is_empty() ); } #[test] fn validators_are_required_per_qualified_operation_and_preconditions_precede_permission() { let fixture = Fixture::new(); let view = fixture.engine.view(&Principal::Limited); let validator = fixture .observe(&Principal::Limited, "/added", 0) .object .validator; // The same operation name needs a validator on only one interface. view.call_operation(fixture.request("/added", &fixture.read, "read", None)) .unwrap(); for operation in ["read", "change"] { let request = fixture.request("/added", &fixture.custom, operation, None); assert_eq!( view.call_operation(request) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::ValidatorRequired ); } for (operation, supplied, code) in [ ( "denied", Some(b"stale".to_vec()), ProtocolErrorCode::ValidatorMismatch, ), ( "hidden", validator.clone(), ProtocolErrorCode::OperationNotFound, ), ( "denied", validator.clone(), ProtocolErrorCode::PermissionDenied, ), ] { let request = fixture.request("/added", &fixture.custom, operation, supplied); assert_eq!( view.call_operation(request) .unwrap_err() .protocol_error() .code, code ); } let mut request = fixture.request("/added", &fixture.custom, "denied", validator.clone()); request .arguments .insert("unexpected".into(), Value::String(POLICY_SECRET.into())); assert_eq!( view.call_operation(request) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::InvalidArguments ); view.call_operation(fixture.request("/added", &fixture.custom, "read", validator)) .unwrap(); assert_eq!( *fixture.shared.node("/added").calls.lock().unwrap(), vec!["default.read", "custom.read"] ); } #[test] fn registered_interface_validators_are_independent_call_preconditions() { let fixture = Fixture::configured( false, false, false, Some(Validator::new(b"schema-1".to_vec())), ResourceLimits::default(), ); let fetched = fixture .engine .view(&Principal::Admin) .fetch_interface(&fetch_request(&fixture.custom)) .unwrap(); assert_eq!(fetched.validator, Some(b"schema-1".to_vec())); let view = fixture.engine.view(&Principal::Limited); let mut request = fixture.request("/added", &fixture.custom, "hidden", None); assert_eq!( view.call_operation(request.clone()) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::InterfaceValidatorRequired ); request.interface.validator = Some(b"schema-old".to_vec()); assert_eq!( view.call_operation(request.clone()) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::InterfaceValidatorMismatch ); request.interface.validator = fetched.validator; assert_eq!( view.call_operation(request) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::OperationNotFound ); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); } #[test] fn backend_refreshes_validators_under_execution_lock_before_dispatch() { let fixture = Fixture::new(); let validator = fixture .observe(&Principal::Admin, "/added", 0) .object .validator; fixture.shared.controls.lock().unwrap().bump_before_execute = true; let mut request = fixture.request("/added", &fixture.custom, "change", validator); request .arguments .insert("contents".into(), Value::Bytes(b"should not run".to_vec())); let (response, decoded) = fixture.http_call( &Principal::Admin, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &ExecutionContext::new(), ); wire_error(decoded, ProtocolErrorCode::ValidatorMismatch); no_secrets(&response); let node = fixture.shared.node("/added"); assert!(node.calls.lock().unwrap().is_empty()); assert_eq!(node.data.lock().unwrap().bytes, b"hello memory"); assert_ne!( fixture .observe(&Principal::Admin, "/added", 0) .object .validator, request.target.validator ); } #[test] fn operation_keeps_the_resolved_target_and_returns_its_committed_post_validator() { let fixture = Fixture::new(); let original = fixture.shared.node("/added"); let validator = fixture .observe(&Principal::Admin, "/added", 0) .object .validator; fixture.shared.controls.lock().unwrap().replace_mapping = true; let mut request = fixture.request("/added", &fixture.custom, "change", validator.clone()); request .arguments .insert("contents".into(), Value::Bytes(b"committed".to_vec())); let (_, decoded) = fixture.http_call( &Principal::Admin, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &ExecutionContext::new(), ); let result = success(decoded); assert_eq!(result.result, Value::Unit); assert_ne!(result.validator, validator); assert_eq!( result.validator, Some( Entry::snapshot("/added".into(), original.clone()) .validator .as_bytes() .to_vec() ) ); assert_eq!(original.data.lock().unwrap().bytes, b"committed"); assert_eq!(*original.calls.lock().unwrap(), vec!["custom.change"]); let decoy = fixture.shared.node("/added"); assert!(!Arc::ptr_eq(&original, &decoy)); assert_eq!(decoy.data.lock().unwrap().bytes, b"decoy"); assert!(decoy.calls.lock().unwrap().is_empty()); } #[test] fn concurrent_conditional_calls_are_serialized_across_validation_and_commit() { let fixture = Fixture::new(); let validator = fixture .observe(&Principal::Admin, "/added", 0) .object .validator; let barrier = Barrier::new(2); let results = std::thread::scope(|scope| { let mut workers = Vec::new(); for bytes in [b"first".to_vec(), b"second".to_vec()] { let mut request = fixture.request("/added", &fixture.custom, "change", validator.clone()); request .arguments .insert("contents".into(), Value::Bytes(bytes)); let engine = &fixture.engine; let barrier = &barrier; workers.push(scope.spawn(move || { barrier.wait(); engine.view(&Principal::Admin).call_operation(request) })); } workers .into_iter() .map(|worker| worker.join().unwrap()) .collect::>() }); assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); assert_eq!( results .iter() .filter_map(|result| result.as_ref().err()) .next() .unwrap() .protocol_error() .code, ProtocolErrorCode::ValidatorMismatch ); assert_eq!( *fixture.shared.node("/added").calls.lock().unwrap(), vec!["custom.change"] ); let returned = results.into_iter().find_map(Result::ok).unwrap().validator; assert_eq!( returned, fixture .observe(&Principal::Admin, "/added", 0) .object .validator ); } #[test] fn explicit_effect_guarantees_and_confirmed_rollback_control_failure_classification() { for rollback in [false, true] { for mode in 0..4 { let fixture = Fixture::new(); fixture.shared.controls.lock().unwrap().rollback = rollback; let before = fixture.shared.node("/added").data.lock().unwrap().clone(); let before_validator = fixture .observe(&Principal::Admin, "/added", 0) .object .validator; let mut request = fixture.request("/added", &fixture.custom, "fail", None); request .arguments .insert("mode".into(), Value::Integer(mode)); let (response, decoded) = fixture.http_call( &Principal::Admin, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &ExecutionContext::new(), ); let code = if mode >= 2 && !rollback { ProtocolErrorCode::OperationOutcomeUnknown } else if mode % 2 == 0 { ProtocolErrorCode::Internal } else { ProtocolErrorCode::ResourceLimitExceeded }; wire_error(decoded, code); no_secrets(&response); let after = fixture.shared.node("/added").data.lock().unwrap().clone(); assert_eq!( *fixture.shared.node("/added").calls.lock().unwrap(), vec!["custom.fail"] ); if rollback || mode < 2 { assert_eq!(after, before, "mode={mode}, rollback={rollback}"); assert_eq!( fixture .observe(&Principal::Admin, "/added", 0) .object .validator, before_validator ); } else { assert_eq!(after.bytes, b"uncertain effects"); assert_ne!(after.revision, before.revision); } } } } #[test] fn backend_boundary_failures_never_upgrade_unknown_effects() { for (guarantee, code) in [ (EffectGuarantee::NotCommitted, ProtocolErrorCode::Internal), ( EffectGuarantee::Unknown, ProtocolErrorCode::OperationOutcomeUnknown, ), ] { let fixture = Fixture::new(); fixture.shared.controls.lock().unwrap().execute_error = Some(guarantee); let request = fixture.request("/added", &fixture.custom, "denied", None); let (response, decoded) = fixture.http_call( &Principal::Admin, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &ExecutionContext::new(), ); wire_error(decoded, code); no_secrets(&response); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); } } #[test] fn backend_and_composition_secret_diagnostics_are_sanitized_on_observe_and_call() { for stage in ["resolve", "children", "publish"] { let fixture = Fixture::new(); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); { let mut controls = fixture.shared.controls.lock().unwrap(); match stage { "resolve" => controls.resolve_error = true, "children" => controls.children_error = true, "publish" => controls.publish_error = true, _ => unreachable!(), } } let view = fixture.engine.view(&Principal::Admin); let request = observe_request("/", 1); assert_eq!( view.observe(&ExecutionContext::new(), &request) .unwrap_err() .code, ProtocolErrorCode::Internal ); let response = HttpAdapter::new(&view, &endpoint, fixture.limits).handle_http_with_context( encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(), &ExecutionContext::new(), ); wire_error( decode_observe_response(&request, &response, fixture.limits.wire).unwrap(), ProtocolErrorCode::Internal, ); no_secrets(&response); if stage != "children" { let request = fixture.request("/added", &fixture.custom, "denied", None); let (response, decoded) = fixture.http_call( &Principal::Admin, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &ExecutionContext::new(), ); wire_error(decoded, ProtocolErrorCode::Internal); no_secrets(&response); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); } } } #[test] fn policy_panics_and_guessed_operation_diagnostics_do_not_leak_to_http() { let fixture = Fixture::configured(false, false, true, None, ResourceLimits::default()); let request = fixture.request("/added", &fixture.custom, "denied", None); let (response, decoded) = fixture.http_call( &Principal::Limited, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &ExecutionContext::new(), ); wire_error(decoded, ProtocolErrorCode::OperationOutcomeUnknown); no_secrets(&response); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); let fixture = Fixture::new(); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let view = fixture.engine.view(&Principal::Limited); // This valid metadata names a nonexistent operation; bypass client-side // descriptor validation to exercise server diagnostics for untrusted names. let body = format!( r#"{{"target":{{"path":"/added"}},"interface":{{"reference":{{"scope":"{}","name":"{}"}}}},"operation":"{}","arguments":{{}}}}"#, fixture.custom.scope, fixture.custom.name, POLICY_SECRET ); let request = http::Request::builder() .method(http::Method::POST) .uri(endpoint.route(wip_http::Route::CallOperation).as_str()) .header(http::header::CONTENT_TYPE, wip_http::JSON_CONTENT_TYPE) .body(body.into_bytes()) .unwrap(); let response = HttpAdapter::new(&view, &endpoint, fixture.limits) .handle_http_with_context(request, &ExecutionContext::new()); no_secrets(&response); assert!( std::str::from_utf8(response.body()) .unwrap() .contains("operation_not_found") ); } #[test] fn http_context_reaches_resolve_children_publication_boundary_and_typed_operation_unchanged() { let fixture = Fixture::new(); let token = CancellationToken::new(); let deadline = Instant::now() + Duration::from_secs(60); let context = ExecutionContext::new() .with_cancellation(token.clone()) .with_deadline(deadline) .with_resource_budget(77); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let view = fixture.engine.view(&Principal::Admin); let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits); let request = observe_request("/", 1); let response = adapter.handle_http_with_context( encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(), &context, ); success(decode_observe_response(&request, &response, fixture.limits.wire).unwrap()); let request = fixture.request("/added", &fixture.read, "read", None); let (_, decoded) = fixture.http_call( &Principal::Admin, &request, &fixture.descriptor(&Principal::Admin, &fixture.read), &context, ); success(decoded); let seen = fixture.shared.seen.lock().unwrap(); for stage in ["resolve", "children", "publish", "execute", "typed"] { assert!( seen.iter().any(|record| record.stage == stage), "missing {stage}" ); } for record in seen.iter() { assert_eq!(record.deadline, Some(deadline), "{}", record.stage); assert_eq!(record.budget, Some(77), "{}", record.stage); assert!(!record.token.is_cancelled()); } token.cancel(); assert!( seen.iter().all(|record| record.token.is_cancelled()), "a context stage received an unrelated cancellation token" ); } #[test] fn cancelled_elapsed_and_empty_budget_requests_reject_without_backend_work() { for condition in ["cancelled", "deadline", "budget"] { let fixture = Fixture::new(); let token = CancellationToken::new(); let context = match condition { "cancelled" => { token.cancel(); ExecutionContext::new().with_cancellation(token) } "deadline" => { ExecutionContext::new().with_deadline(Instant::now() - Duration::from_secs(1)) } "budget" => ExecutionContext::new().with_resource_budget(0), _ => unreachable!(), }; let view = fixture.engine.view(&Principal::Admin); let request = observe_request("/added", 0); assert_eq!( view.observe(&context, &request).unwrap_err().code, ProtocolErrorCode::ResourceLimitExceeded ); let call = fixture.request("/added", &fixture.read, "read", None); assert_eq!( view.call_operation_with_context(&context, call.clone()) .unwrap_err() .protocol_error() .code, ProtocolErrorCode::ResourceLimitExceeded ); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits); let response = adapter.handle_http_with_context( encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(), &context, ); if condition == "deadline" { assert_eq!(response.status(), http::StatusCode::GATEWAY_TIMEOUT); assert!(response.body().is_empty()); } else { wire_error( decode_observe_response(&request, &response, fixture.limits.wire).unwrap(), ProtocolErrorCode::ResourceLimitExceeded, ); let (_, decoded) = fixture.http_call( &Principal::Admin, &call, &fixture.descriptor(&Principal::Admin, &fixture.read), &context, ); wire_error(decoded, ProtocolErrorCode::ResourceLimitExceeded); } assert!(fixture.shared.seen.lock().unwrap().is_empty()); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); } } #[test] fn cancellation_or_visibility_change_inside_boundary_prevents_typed_dispatch() { for condition in ["execute", "publish", "visibility"] { let fixture = Fixture::new(); { let mut controls = fixture.shared.controls.lock().unwrap(); match condition { "execute" => controls.cancel_on_execute = true, "publish" => controls.cancel_on_publish = true, "visibility" => controls.hide_before_execute = true, _ => unreachable!(), } } let request = fixture.request("/added", &fixture.custom, "denied", None); let context = ExecutionContext::new(); let (response, decoded) = fixture.http_call( &Principal::Admin, &request, &fixture.descriptor(&Principal::Admin, &fixture.custom), &context, ); wire_error( decoded, if condition == "visibility" { ProtocolErrorCode::NotFound } else { ProtocolErrorCode::ResourceLimitExceeded }, ); no_secrets(&response); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); if condition != "visibility" { assert!(context.cancellation().is_cancelled()); } } } #[test] fn observation_depth_node_and_context_budgets_fail_without_truncating_the_tree() { let limits = ResourceLimits { max_observation_depth: 1, max_observation_nodes: 2, ..ResourceLimits::default() }; let fixture = Fixture::configured(false, false, false, None, limits); let view = fixture.engine.view(&Principal::Admin); assert_eq!( view.observe(&ExecutionContext::new(), &observe_request("/", 2)) .unwrap_err() .code, ProtocolErrorCode::ResourceLimitExceeded ); assert!( fixture.shared.seen.lock().unwrap().is_empty(), "depth limit should precede resolution" ); assert_eq!( view.observe(&ExecutionContext::new(), &observe_request("/", 1)) .unwrap_err() .code, ProtocolErrorCode::ResourceLimitExceeded ); assert!( view.observe(&ExecutionContext::new(), &observe_request("/added", 1)) .unwrap() .children .unwrap() .is_empty() ); let fixture = Fixture::new(); let view = fixture.engine.view(&Principal::Limited); let context = ExecutionContext::new().with_resource_budget(2); assert_eq!( view.observe(&context, &observe_request("/", 1)) .unwrap_err() .code, ProtocolErrorCode::ResourceLimitExceeded ); let endpoint = Endpoint::parse("https://example.test/extensions").unwrap(); let request = observe_request("/", 1); let response = HttpAdapter::new(&view, &endpoint, fixture.limits).handle_http_with_context( encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(), &context, ); wire_error( decode_observe_response(&request, &response, fixture.limits.wire).unwrap(), ProtocolErrorCode::ResourceLimitExceeded, ); no_secrets(&response); } #[test] fn root_scope_fetch_and_call_use_the_current_boundary_snapshot() { let fixture = Fixture::new(); let view = fixture.engine.view(&Principal::Admin); let fetched = view .fetch_interface(&fetch_request(&fixture.custom)) .unwrap(); assert_eq!(fetched.scope_ref.as_deref(), Some("memory-root")); assert!( fixture .observe(&Principal::Admin, "/", 0) .object .interfaces .is_empty() ); let mut request = fixture.request("/added", &fixture.custom, "denied", None); request.interface.scope_ref = Some("wrong-root".into()); assert_eq!( view.call_operation(request.clone()).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceMismatch ); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); // Ordinary state updates do not replace the publication lifetime or ref. fixture.shared.node("/").data.lock().unwrap().revision += 1; request.interface.scope_ref = fetched.scope_ref; view.call_operation(request).unwrap(); // Fetch cannot attest a stale prelookup: visibility is refreshed under the // same boundary even though resolve initially sees a visible root. fixture.shared.controls.lock().unwrap().hide_before_execute = true; assert_eq!( view.fetch_interface(&fetch_request(&fixture.custom)) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); } #[test] fn root_scope_replacement_during_call_rejects_the_prelookup() { let fixture = Fixture::new(); let view = fixture.engine.view(&Principal::Admin); // Simulate a retained non-root lineage whose root is replaced before call. fixture.shared.controls.lock().unwrap().replace_ancestor = Some("/".into()); let request = fixture.request("/added", &fixture.custom, "denied", None); assert_eq!( view.call_operation(request).unwrap_err().kind(), wip_host::CallErrorKind::NotFound ); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); assert_eq!( view.fetch_interface(&fetch_request(&fixture.custom)) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); let fixture = Fixture::new(); fixture.shared.controls.lock().unwrap().replace_ancestor = Some("/".into()); assert_eq!( fixture .engine .view(&Principal::Admin) .fetch_interface(&fetch_request(&fixture.custom)) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); } #[path = "support/scoped.rs"] mod scoped; #[test] fn nonroot_registrations_fail_closed_without_composition_lifetime_attestation() { let fixture = Fixture::new(); let mut registry = InterfaceRegistry::new(); let root = Scope::new("/").unwrap(); let local = Scope::new("/added").unwrap(); let defaults = DefaultInterfaces::register(&mut registry, &root).unwrap(); let custom = registry .register( &local, "memory.operations", MemoryOperationsDefinition, None, ) .unwrap(); let reference = custom.reference().protocol_reference(); let policy = Policy { custom: custom.reference().clone(), read: defaults.read.reference().clone(), hide_read: false, invalid_selection: false, panic_authorize: false, }; let engine = Projection::new( MemoryBackend::new(fixture.shared.clone()), registry, MemoryComposition { defaults, custom, shared: fixture.shared.clone(), }, policy, fixture.limits, ); let view = engine.view(&Principal::Admin); let object = view.observe_object("/added").unwrap(); assert!(!object.interfaces.contains(&reference)); assert_eq!( view.fetch_interface(&fetch_request(&reference)) .unwrap_err() .code, ProtocolErrorCode::InterfaceNotFound ); let request = fixture.request("/added", &reference, "denied", None); assert_eq!( view.call_operation(request).unwrap_err().kind(), wip_host::CallErrorKind::InterfaceMismatch ); assert!( fixture .shared .node("/added") .calls .lock() .unwrap() .is_empty() ); }