use super::*;
use objc2_foundation::NSURL;
#[cfg(feature = "local-server")]
#[test]
fn idle_native_cursor_is_retired_by_the_runloop_without_another_message() {
use objc2_foundation::{NSDate, NSRunLoop};
use std::time::{Duration, Instant};
let owner = owner();
let state = MacIpc::new(owner.bridge());
state.commit_for_test();
state.data.borrow_mut().seed_test_incoming(
owner.bridge().reserve_input(4096).unwrap(),
Instant::now() + Duration::from_millis(20),
);
state.arm_data_deadline();
assert!(state.data_timer.borrow().is_some());
NSRunLoop::currentRunLoop().runUntilDate(&NSDate::dateWithTimeIntervalSinceNow(0.1));
assert!(state.data.borrow().next_deadline().is_none());
assert!(state.data_timer.borrow().is_none());
state.close();
}
#[cfg(feature = "local-server")]
#[test]
fn idle_native_output_lease_is_released_by_the_runloop() {
use objc2_foundation::{NSDate, NSRunLoop};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
let owner = owner();
let state = MacIpc::new(owner.bridge());
state.commit_for_test();
let dropped = Arc::new(AtomicBool::new(false));
state.data.borrow_mut().seed_test_outgoing(
crate::DesktopResponseBody::with_guard(vec![1; 4096], DropFlag(Arc::clone(&dropped))),
Instant::now() + Duration::from_millis(20),
);
state.arm_data_deadline();
NSRunLoop::currentRunLoop().runUntilDate(&NSDate::dateWithTimeIntervalSinceNow(0.1));
assert!(dropped.load(Ordering::Acquire));
assert!(state.data_timer.borrow().is_none());
state.close();
}
fn owner() -> crate::ipc::IpcWindowOwner {
crate::ipc::IpcWindowOwner::new(
std::sync::Arc::new(crate::ipc::IpcRegistry::default()),
crate::ipc::IpcOptions::default(),
crate::ipc::IpcHost::Source {
origin: super::super::APP_ORIGIN.into(),
},
)
.unwrap()
}
#[test]
fn canonical_origin_rejects_lookalikes_credentials_and_ports() {
for input in [
"webui://evil/",
"webui://app.evil/",
"webui://app:9/",
"webui://user@app/",
"https://app/",
"about:blank",
] {
let url = NSURL::URLWithString(&NSString::from_str(input)).unwrap();
assert!(!trusted_url(&url), "{input}");
}
assert!(trusted_url(
&NSURL::URLWithString(&NSString::from_str("webui://app/path?q=x")).unwrap()
));
}
#[test]
fn late_same_url_probes_and_proofs_cannot_activate_a_replacement() {
let owner = owner();
let state = MacIpc::new(owner.bridge());
state.commit_for_test();
let old = DocumentActivation {
navigation: state.navigation(),
document_nonce: [1; 16],
challenge: [2; 16],
};
*state.proof.borrow_mut() = Some(old.clone());
assert!(state.accepts_proof(&old));
state.commit_for_test();
assert!(!state.is_current(old.navigation));
assert!(!state.accepts_proof(&old));
let new = DocumentActivation {
navigation: state.navigation(),
document_nonce: [3; 16],
challenge: [4; 16],
};
*state.proof.borrow_mut() = Some(new.clone());
let forged = DocumentActivation {
navigation: new.navigation,
..old
};
assert!(!state.accepts_proof(&forged));
assert!(state.accepts_proof(&new));
state.navigate();
assert!(!state.accepts_proof(&new));
state.close();
}
#[test]
fn trusted_navigation_publishes_outgoing_retirement_before_cancelling_responses() {
struct Completion(Rc<RefCell<Vec<&'static str>>>);
impl Drop for Completion {
fn drop(&mut self) {
self.0.borrow_mut().push("response-cancelled");
}
}
let owner = owner();
let state = MacIpc::new(owner.bridge());
state.commit_for_test();
let proof = DocumentActivation {
navigation: state.navigation(),
document_nonce: [1; 16],
challenge: [2; 16],
};
*state.proof.borrow_mut() = Some(proof.clone());
*state.session.borrow_mut() = Some(SessionInfo {
generation: 7,
token: "a".repeat(32),
limits: crate::ipc::IpcLimits::default(),
});
let order = Rc::new(RefCell::new(Vec::new()));
let completion = Completion(Rc::clone(&order));
let tasks = Rc::clone(&state.tasks.borrow());
tasks
.spawn(async move {
let _completion = completion;
std::future::pending::<()>().await;
})
.unwrap();
state.navigation_started_with(|outgoing, generation| {
assert_eq!(outgoing, proof);
assert_eq!(generation, 7);
assert!(state.is_current(proof.navigation));
let script = crate::native_ipc::control_script(
&outgoing,
NativeControl::Closed {
generation,
code: IpcErrorCode::Navigated,
},
)
.unwrap();
assert!(script.contains("\"code\":\"navigated\""));
assert!(!script.contains("challenge"));
order.borrow_mut().push("retirement-queued");
});
assert_eq!(*order.borrow(), ["retirement-queued", "response-cancelled"]);
assert!(!state.is_current(proof.navigation));
assert!(state.proof.borrow().is_none());
assert!(state.session.borrow().is_none());
assert_eq!(
tasks.spawn(async {}).unwrap_err().code,
IpcErrorCode::Closed
);
let duplicate = Cell::new(false);
state.navigation_started_with(|_, _| duplicate.set(true));
assert!(!duplicate.get());
state.close();
}