use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use rightkit_tauri_shell::broker::PathBroker;
#[cfg(unix)]
use rightkit_tauri_shell::site::resolve_in_root;
use rightkit_tauri_shell::site::{serve_site_request, SitePolicy};
use rightkit_tauri_shell::stream::{serve as serve_stream, StreamPolicy};
use tauri::http::{Request, Response, StatusCode};
fn fixture(dir: &tempfile::TempDir, name: &str, body: &[u8]) -> PathBuf {
let path = dir.path().join(name);
std::fs::write(&path, body).unwrap();
path
}
fn canonical(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap()
}
fn has_random_hex_suffix(token: &str, prefix: &str) -> bool {
let Some(suffix) = token.strip_prefix(prefix) else {
return false;
};
suffix.len() == 32 && suffix.chars().all(|c| c.is_ascii_hexdigit())
}
fn site_sync(broker: &PathBroker, request: &Request<Vec<u8>>) -> Response<Vec<u8>> {
let (tx, rx) = mpsc::channel();
serve_site_request(broker, &SitePolicy::default(), request, move |response| {
let _ = tx.send(response);
});
rx.recv_timeout(Duration::from_secs(5))
.expect("serve_site_request responds exactly once")
}
fn stream_sync(broker: &PathBroker, request: &Request<Vec<u8>>) -> Response<Vec<u8>> {
let (tx, rx) = mpsc::channel();
serve_stream(broker, &StreamPolicy::default(), request, move |response| {
let _ = tx.send(response);
});
rx.recv_timeout(Duration::from_secs(5))
.expect("serve responds exactly once")
}
fn site_get(token: &str, rel: &str) -> Request<Vec<u8>> {
Request::builder()
.uri(format!("viewright-site://localhost/{token}/{rel}"))
.body(Vec::new())
.unwrap()
}
#[test]
fn known_token_does_not_grant_until_exchanged() {
let dir = tempfile::tempdir().unwrap();
let path = fixture(&dir, "known-token.txt", b"ok");
let broker = PathBroker::default();
let token = broker.register_known_path(path.to_str().unwrap()).unwrap();
assert!(broker.require_file(path.to_str().unwrap()).is_err());
let granted = broker.grant_known(&token).unwrap();
assert_eq!(
broker.require_file(path.to_str().unwrap()).unwrap(),
granted
);
assert!(broker.grant_known(&token).is_err());
}
#[test]
fn stored_known_paths_are_canonicalized_before_granting() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("known-canonical-dir");
std::fs::create_dir_all(&sub).unwrap();
let path = sub.join("known-canonical.txt");
std::fs::write(&path, "ok").unwrap();
let non_canonical = sub
.join("..")
.join("known-canonical-dir")
.join("known-canonical.txt");
let broker = PathBroker::default();
let token = broker
.register_known_path(non_canonical.to_str().unwrap())
.unwrap();
let granted = broker.grant_known(&token).unwrap();
assert_eq!(
broker.require_file(path.to_str().unwrap()).unwrap(),
granted
);
}
#[test]
fn stream_token_is_revoked_with_path_and_not_revived_by_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = fixture(&dir, "stream-token.pdf", b"%PDF-1.7\n");
let broker = PathBroker::default();
let granted = broker.grant_file(&path).unwrap();
let id = broker.grant_stream(&granted).unwrap();
assert_eq!(broker.resolve_stream(&id).unwrap().path, canonical(&path));
broker.revoke_path(&granted).unwrap();
assert!(broker.resolve_stream(&id).is_err());
broker.grant_file(&path).unwrap();
assert!(broker.require_file(path.to_str().unwrap()).is_ok());
assert!(broker.resolve_stream(&id).is_err());
}
#[test]
fn site_root_token_resolves_until_revoked() {
let dir = tempfile::tempdir().unwrap();
let broker = PathBroker::default();
let token = broker.grant_site_root(dir.path()).unwrap();
assert!(has_random_hex_suffix(&token, "site-"));
assert_eq!(
broker.resolve_site_root(&token).unwrap(),
canonical(dir.path())
);
broker.revoke_site_root(&token).unwrap();
assert!(broker.resolve_site_root(&token).is_err());
broker.revoke_site_root(&token).unwrap(); }
#[test]
fn site_root_grant_is_not_a_file_grant() {
let dir = tempfile::tempdir().unwrap();
let secret = fixture(&dir, "secret.txt", b"s");
let broker = PathBroker::default();
broker.grant_site_root(dir.path()).unwrap();
assert!(broker.require_file(secret.to_str().unwrap()).is_err());
}
#[test]
fn two_tabs_on_one_root_get_independent_tokens() {
let dir = tempfile::tempdir().unwrap();
let broker = PathBroker::default();
let tab_a = broker.grant_site_root(dir.path()).unwrap();
let tab_b = broker.grant_site_root(dir.path()).unwrap();
assert_ne!(tab_a, tab_b);
broker.revoke_site_root(&tab_a).unwrap();
assert!(broker.resolve_site_root(&tab_a).is_err());
assert!(broker.resolve_site_root(&tab_b).is_ok());
}
#[cfg(unix)]
#[test]
fn rejects_symlink_pointing_outside_root() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("site");
std::fs::create_dir_all(&root).unwrap();
let outside = dir.path().join("outside.png");
std::fs::write(&outside, b"x").unwrap();
std::os::unix::fs::symlink(&outside, root.join("link.png")).unwrap();
let err = resolve_in_root(&root, "link.png", &SitePolicy::default()).unwrap_err();
assert_eq!(err.code(), "broker_denied", "unexpected error: {err}");
let broker = PathBroker::default();
let token = broker.grant_site_root(&root).unwrap();
assert_eq!(
site_sync(&broker, &site_get(&token, "link.png")).status(),
StatusCode::FORBIDDEN
);
}
#[test]
fn revoked_root_stops_serving_immediately() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.png"), b"x").unwrap();
let broker = PathBroker::default();
let token = broker.grant_site_root(dir.path()).unwrap();
assert_eq!(
site_sync(&broker, &site_get(&token, "a.png")).status(),
StatusCode::OK
);
broker.revoke_site_root(&token).unwrap();
assert_eq!(
site_sync(&broker, &site_get(&token, "a.png")).status(),
StatusCode::FORBIDDEN
);
}
#[test]
fn revoked_token_is_denied_even_if_path_reopens() {
let dir = tempfile::tempdir().unwrap();
let path = fixture(&dir, "revoked.pdf", b"0123456789");
let broker = PathBroker::default();
let granted = broker.grant_file(&path).unwrap();
let id = broker.grant_stream(&granted).unwrap();
broker.revoke_path(&granted).unwrap();
broker.grant_file(&path).unwrap();
let request = Request::builder()
.uri(format!("viewright-stream://localhost/{id}"))
.body(Vec::new())
.unwrap();
let response = stream_sync(&broker, &request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert!(response.body().is_empty());
}
#[test]
fn denial_text_names_the_configured_app() {
let dir = tempfile::tempdir().unwrap();
let path = fixture(&dir, "named-app.txt", b"ok");
let default_msg = PathBroker::default()
.require_file(path.to_str().unwrap())
.unwrap_err()
.to_string();
assert!(
default_msg.contains("through the app first"),
"{default_msg}"
);
let named_msg = PathBroker::with_app_name("ViewRight")
.require_file(path.to_str().unwrap())
.unwrap_err()
.to_string();
assert!(named_msg.contains("through ViewRight first"), "{named_msg}");
}
#[cfg(any(windows, target_os = "macos"))]
#[test]
fn revoke_ignores_case_on_case_insensitive_platforms() {
let dir = tempfile::tempdir().unwrap();
let path = fixture(&dir, "Case-Revoke.txt", b"ok");
let broker = PathBroker::default();
let display = broker.grant_file(&path).unwrap();
std::fs::remove_file(&path).unwrap();
broker.revoke_path(&display.to_uppercase()).unwrap();
let normalized = std::fs::canonicalize(dir.path())
.unwrap()
.join("Case-Revoke.txt");
assert!(!broker.is_granted_normalized(&normalized).unwrap());
}