use super::*;
#[test]
fn test_extract_body_http() {
let http = b"POST / HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{\"jsonrpc\":\"2.0\"}";
assert_eq!(extract_body(http), br#"{"jsonrpc":"2.0"}"#);
}
#[test]
fn test_extract_body_raw_json() {
let raw = br#"{"jsonrpc":"2.0"}"#;
assert_eq!(extract_body(raw), br#"{"jsonrpc":"2.0"}"#);
}
#[tokio::test]
#[serial_test::serial]
async fn test_dispatch_rejects_in_alternate_mode() {
use super::super::protocol::R_NOT_AT_PROMPT;
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
super::super::set_in_alternate_mode(false);
}
}
super::super::set_in_alternate_mode(true);
let _guard = Guard;
let (tx, _rx) = mpsc::channel();
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
method: "evaluate".to_string(),
params: serde_json::json!({"code": "1+1"}),
id: Some(serde_json::json!(1)),
};
let response = dispatch_request(request, &tx).await;
assert_eq!(response.error.unwrap().code, R_NOT_AT_PROMPT);
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
method: "user_input".to_string(),
params: serde_json::json!({"code": "print('hello')"}),
id: Some(serde_json::json!(2)),
};
let response = dispatch_request(request, &tx).await;
assert_eq!(response.error.unwrap().code, R_NOT_AT_PROMPT);
}
#[tokio::test]
#[serial_test::serial]
async fn test_session_returns_arf_only_in_alternate_mode() {
use super::super::protocol::SessionResult;
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
super::super::set_in_alternate_mode(false);
}
}
super::super::set_in_alternate_mode(true);
let _guard = Guard;
let (tx, _rx) = mpsc::channel();
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
method: "session".to_string(),
params: serde_json::json!({}),
id: Some(serde_json::json!(1)),
};
let response = dispatch_request(request, &tx).await;
assert!(
response.error.is_none(),
"session should not return an error"
);
let result_value = response.result.expect("session should return a result");
let result: SessionResult =
serde_json::from_value(result_value).expect("should parse as SessionResult");
assert!(!result.arf_version.is_empty());
assert!(result.pid > 0);
assert!(
result.r.is_none(),
"R info should be null in alternate mode"
);
let reason = result
.r_unavailable_reason
.expect("should have r_unavailable_reason");
assert!(
reason.contains("alternate mode"),
"reason should mention alternate mode, got: {reason}"
);
assert!(result.hint.is_some(), "should have a hint");
}
#[tokio::test]
#[serial_test::serial]
async fn test_session_fallback_on_channel_failure() {
use super::super::protocol::SessionResult;
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
super::super::set_in_alternate_mode(false);
}
}
super::super::set_in_alternate_mode(false);
let _guard = Guard;
let (tx, _rx) = mpsc::channel();
drop(_rx);
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
method: "session".to_string(),
params: serde_json::json!({}),
id: Some(serde_json::json!(1)),
};
let response = dispatch_request(request, &tx).await;
assert!(
response.error.is_none(),
"session should not return an error"
);
let result_value = response.result.expect("session should return a result");
let result: SessionResult =
serde_json::from_value(result_value).expect("should parse as SessionResult");
assert!(result.r.is_none(), "R info should be null");
assert!(
result.r_unavailable_reason.is_some(),
"should have r_unavailable_reason"
);
}
#[test]
#[serial_test::serial]
fn test_session_result_includes_log_file() {
super::super::set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
Some("/opt/R/4.4.1/lib/R".to_string()),
Some("/tmp/arf.log".to_string()),
None,
SessionType::Interactive,
);
let result = super::super::collect_session_result(false, "test");
assert_eq!(result.log_file.as_deref(), Some("/tmp/arf.log"));
super::super::set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
None,
None,
None,
SessionType::Interactive,
);
let result = super::super::collect_session_result(false, "test");
assert_eq!(result.log_file, None);
let json = serde_json::to_value(&result).unwrap();
assert!(
json.get("log_file").is_some(),
"log_file field should always be present in JSON"
);
assert!(
json["log_file"].is_null(),
"log_file should be null when not configured"
);
}
#[test]
#[serial_test::serial]
fn test_session_result_includes_r_home() {
super::super::set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
Some("/opt/R/4.4.1/lib/R".to_string()),
None,
None,
SessionType::Interactive,
);
let result = super::super::collect_session_result(false, "test");
assert_eq!(result.r_home.as_deref(), Some("/opt/R/4.4.1/lib/R"));
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["r_home"], "/opt/R/4.4.1/lib/R");
super::super::set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
None,
None,
None,
SessionType::Interactive,
);
let result = super::super::collect_session_result(false, "test");
assert_eq!(result.r_home, None);
let json = serde_json::to_value(&result).unwrap();
assert!(json.get("r_home").is_some());
assert!(json["r_home"].is_null());
}
#[test]
#[serial_test::serial]
fn test_session_result_includes_history_session_id() {
let session_id: i64 = 1_700_000_000_000_000_000;
super::super::set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
None,
None,
Some(session_id),
SessionType::Interactive,
);
let result = super::super::collect_session_result(false, "test");
assert_eq!(result.history_session_id, Some(session_id));
assert_eq!(
result.ipc_policy.silent,
crate::ipc::policy::SilentPolicy::Restricted {
allowed_functions: Vec::new()
}
);
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["history_session_id"], session_id);
assert_eq!(json["ipc_policy"]["silent"]["mode"], "restricted");
super::super::set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
None,
None,
None,
SessionType::Interactive,
);
let result = super::super::collect_session_result(false, "test");
assert_eq!(result.history_session_id, None);
let json = serde_json::to_value(&result).unwrap();
assert!(
json["history_session_id"].is_null(),
"history_session_id should be null when not set"
);
}
#[cfg(unix)]
mod socket_dir_tests {
use super::super::{is_dir_safe, random_hex_suffix, select_socket_dir};
use std::os::unix::fs::PermissionsExt;
#[test]
fn random_hex_suffix_has_expected_format() {
let suffix = random_hex_suffix();
assert_eq!(suffix.len(), 16);
assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn nonexistent_dir_is_safe() {
let tmp = tempfile::tempdir().unwrap();
let candidate = tmp.path().join("does-not-exist");
assert!(is_dir_safe(&candidate));
}
#[test]
fn dir_with_0700_is_safe() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("good");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(is_dir_safe(&dir));
}
#[test]
fn dir_with_group_read_is_unsafe() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("leaky");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o750)).unwrap();
assert!(!is_dir_safe(&dir));
}
#[test]
fn dir_with_other_write_is_unsafe() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("world");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o702)).unwrap();
assert!(!is_dir_safe(&dir));
}
#[test]
fn symlink_is_unsafe() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("real");
std::fs::create_dir(&target).unwrap();
let link = tmp.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(!is_dir_safe(&link));
}
#[test]
fn regular_file_is_unsafe() {
let tmp = tempfile::tempdir().unwrap();
let file = tmp.path().join("not-a-dir");
std::fs::write(&file, "").unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(!is_dir_safe(&file));
}
#[test]
fn select_uses_existing_safe_candidate() {
let tmp = tempfile::tempdir().unwrap();
let existing = tmp.path().join("existing");
std::fs::create_dir(&existing).unwrap();
std::fs::set_permissions(&existing, std::fs::Permissions::from_mode(0o700)).unwrap();
let fallback = tmp.path().join("fallback");
let (path, created) =
select_socket_dir(12345, &[existing.clone(), fallback.clone()]).unwrap();
assert!(path.contains("existing"));
assert!(
!created,
"dir already existed, should not report as created"
);
assert!(!fallback.exists(), "fallback should not have been created");
}
#[test]
fn select_uses_first_safe_candidate() {
let tmp = tempfile::tempdir().unwrap();
let good = tmp.path().join("good");
let also_good = tmp.path().join("also-good");
let (path, created) = select_socket_dir(12345, &[good.clone(), also_good.clone()]).unwrap();
assert!(path.contains("good"));
assert!(created, "dir did not exist, should report as created");
assert!(good.exists(), "first candidate should have been created");
assert!(
!also_good.exists(),
"second candidate should not have been created"
);
}
#[test]
fn select_skips_unsafe_candidate() {
let tmp = tempfile::tempdir().unwrap();
let bad = tmp.path().join("bad");
std::fs::create_dir(&bad).unwrap();
std::fs::set_permissions(&bad, std::fs::Permissions::from_mode(0o777)).unwrap();
let good = tmp.path().join("fallback");
let (path, created) = select_socket_dir(12345, &[bad, good.clone()]).unwrap();
assert!(path.contains("fallback"));
assert!(created, "fallback dir should have been created");
}
#[test]
fn select_returns_none_when_all_unsafe() {
let tmp = tempfile::tempdir().unwrap();
let bad1 = tmp.path().join("bad1");
std::fs::create_dir(&bad1).unwrap();
std::fs::set_permissions(&bad1, std::fs::Permissions::from_mode(0o777)).unwrap();
let bad2 = tmp.path().join("bad2");
std::fs::write(&bad2, "").unwrap(); std::fs::set_permissions(&bad2, std::fs::Permissions::from_mode(0o700)).unwrap();
let result = select_socket_dir(12345, &[bad1, bad2]);
assert!(result.is_none());
}
}