use std::path::PathBuf;
use solid_pod_rs::error::PodError;
use solid_pod_rs::wac::{
parse_turtle_acl_with_limit, parse_jsonld_acl_with_limits, MAX_ACL_BYTES,
};
use solid_pod_rs::{DotfileAllowlist, is_path_allowed};
use solid_pod_rs::security::dotfile::DotfilePathError;
#[test]
fn acl_max_bytes_is_one_mib() {
assert_eq!(MAX_ACL_BYTES, 1_048_576);
}
#[test]
fn turtle_acl_oversized_returns_payload_too_large() {
let oversized = " ".repeat(MAX_ACL_BYTES + 1);
let result = parse_turtle_acl_with_limit(&oversized, MAX_ACL_BYTES);
assert!(result.is_err());
match result.unwrap_err() {
PodError::PayloadTooLarge(msg) => {
assert!(
msg.contains("exceeds") || msg.contains("too large"),
"error should mention size: {msg}"
);
}
other => panic!("expected PayloadTooLarge, got: {other:?}"),
}
}
#[test]
fn turtle_acl_exactly_at_limit_passes() {
let at_limit = "a".repeat(100);
let result = parse_turtle_acl_with_limit(&at_limit, 100);
assert!(
result.is_ok(),
"exactly at limit should not be rejected: {:?}",
result.unwrap_err()
);
}
#[test]
fn turtle_acl_one_byte_over_limit_rejected() {
let over_limit = "a".repeat(101);
assert!(
parse_turtle_acl_with_limit(&over_limit, 100).is_err(),
"one byte over limit must be rejected"
);
}
#[test]
fn jsonld_acl_oversized_returns_payload_too_large() {
let body = vec![b' '; MAX_ACL_BYTES + 1];
let result = parse_jsonld_acl_with_limits(&body, MAX_ACL_BYTES, 32);
assert!(result.is_err());
match result.unwrap_err() {
PodError::PayloadTooLarge(msg) => {
assert!(
msg.contains("exceeds") || msg.contains("too large"),
"error should mention size: {msg}"
);
}
other => panic!("expected PayloadTooLarge, got: {other:?}"),
}
}
#[test]
fn turtle_acl_valid_within_limits_parses() {
let ttl = r#"
@prefix acl: <http://www.w3.org/ns/auth/acl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
<#public> a acl:Authorization ;
acl:agentClass foaf:Agent ;
acl:accessTo </> ;
acl:mode acl:Read .
"#;
let doc = parse_turtle_acl_with_limit(ttl, MAX_ACL_BYTES).unwrap();
assert!(
doc.graph.is_some(),
"valid ACL should produce a non-empty graph"
);
}
#[test]
fn dotfile_filter_blocks_env() {
let al = DotfileAllowlist::with_defaults();
assert!(
!al.is_allowed(&PathBuf::from("/.env")),
".env must be blocked by default allowlist"
);
}
#[test]
fn dotfile_filter_allows_account() {
let al = DotfileAllowlist::with_defaults();
assert!(
al.is_allowed(&PathBuf::from("/.account")),
".account must be allowed by default allowlist"
);
}
#[test]
fn dotfile_filter_allows_solid_sidecars() {
let al = DotfileAllowlist::with_defaults();
assert!(al.is_allowed(&PathBuf::from("/.acl")));
assert!(al.is_allowed(&PathBuf::from("/.meta")));
}
#[test]
fn free_function_blocks_env() {
match is_path_allowed("/.env") {
Err(DotfilePathError::NotAllowed { segment, .. }) => {
assert_eq!(segment, ".env");
}
other => panic!("expected NotAllowed for /.env, got {other:?}"),
}
}
#[test]
fn free_function_allows_account() {
assert!(
is_path_allowed("/.account").is_ok(),
".account must pass the free-function check"
);
assert!(
is_path_allowed("/.account/login").is_ok(),
".account subtree must pass"
);
}
#[test]
fn dotdot_traversal_rejected_by_dotfile_allowlist() {
let al = DotfileAllowlist::with_defaults();
assert!(
!al.is_allowed(&PathBuf::from("foo/..")),
"parent-dir traversal must be rejected"
);
assert!(
!al.is_allowed(&PathBuf::from("foo/../../etc/passwd")),
"double parent-dir traversal must be rejected"
);
}
#[test]
fn dotdot_bypass_attempt_rejected_by_free_function() {
match is_path_allowed("/pod/../etc/passwd") {
Err(DotfilePathError::ParentTraversal(_)) => {}
other => panic!(
"expected ParentTraversal for /pod/../etc/passwd, got {other:?}"
),
}
}
#[test]
fn nested_hidden_file_blocked() {
assert!(
is_path_allowed("/a/b/.git/HEAD").is_err(),
".git nested deep in path must be blocked"
);
assert!(
is_path_allowed("/pod/.ssh/id_rsa").is_err(),
".ssh must be blocked"
);
}
#[test]
fn empty_and_root_paths_allowed() {
assert!(is_path_allowed("").is_ok());
assert!(is_path_allowed("/").is_ok());
}