use std::sync::Arc;
use solid_pod_rs::storage::memory::MemoryBackend;
use solid_pod_rs_server::{build_app, body_cap_from_env, AppState, NodeInfoMeta, DEFAULT_BODY_CAP};
fn make_state() -> AppState {
let storage = Arc::new(MemoryBackend::new());
AppState::new(storage)
}
#[actix_web::test]
async fn traversal_guard_blocks_dot_dot_slash() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/foo/../etc/passwd")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert!(
rsp.status().is_client_error(),
"path with `../` must be rejected (got {})",
rsp.status().as_u16(),
);
}
#[actix_web::test]
async fn traversal_guard_blocks_encoded_dot_dot() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/foo/%2e%2e/bar")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
400,
"percent-encoded `..` (%2e%2e) must be rejected"
);
}
#[actix_web::test]
async fn traversal_guard_blocks_double_encoded_dot_dot() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/foo/%252e%252e/bar")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
400,
"double-encoded `..` (%252e%252e) must be rejected"
);
}
#[actix_web::test]
async fn traversal_guard_allows_normal_paths() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/alice/docs/readme.txt")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_ne!(
rsp.status().as_u16(),
400,
"normal path must not be rejected by traversal guard"
);
}
#[actix_web::test]
async fn traversal_guard_allows_root() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get().uri("/").to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_ne!(
rsp.status().as_u16(),
400,
"root path must not be rejected by traversal guard"
);
}
#[actix_web::test]
async fn dotfile_guard_blocks_env_path() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get().uri("/.env").to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
403,
"dotfile path `/.env` must be rejected as 403"
);
}
#[actix_web::test]
async fn dotfile_guard_blocks_git_path() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/.git/config")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
403,
"dotfile path `/.git/config` must be rejected as 403"
);
}
#[actix_web::test]
async fn dotfile_guard_allows_acl_path() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/resource/.acl")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_ne!(
rsp.status().as_u16(),
403,
"`.acl` is on the allowlist and must not be blocked"
);
}
#[actix_web::test]
async fn dotfile_guard_allows_meta_path() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/resource/.meta")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_ne!(
rsp.status().as_u16(),
403,
"`.meta` is on the allowlist and must not be blocked"
);
}
#[actix_web::test]
async fn dotfile_guard_allows_account_path() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/.account/login")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_ne!(
rsp.status().as_u16(),
403,
"`.account` is on the default allowlist (Sprint 12) and must not be blocked"
);
}
#[actix_web::test]
async fn dotfile_guard_allows_well_known_paths() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/.well-known/solid")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_ne!(
rsp.status().as_u16(),
403,
"`.well-known/solid` must be allowed through the dotfile guard"
);
assert!(
rsp.status().is_success(),
"`.well-known/solid` should return 200"
);
}
#[actix_web::test]
async fn dotfile_guard_blocks_nested_dotfile() {
let app = actix_web::test::init_service(build_app(make_state())).await;
let req = actix_web::test::TestRequest::get()
.uri("/pod/.secret/data.txt")
.to_request();
let rsp = actix_web::test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
403,
"nested dotfile `.secret` must be blocked"
);
}
#[test]
fn body_cap_default_is_50mb() {
assert_eq!(DEFAULT_BODY_CAP, 50 * 1024 * 1024);
}
#[test]
fn body_cap_from_env_returns_default_on_missing_var() {
std::env::remove_var("JSS_MAX_REQUEST_BODY");
let cap = body_cap_from_env();
assert_eq!(
cap, DEFAULT_BODY_CAP,
"missing env var must fall back to default"
);
}
#[test]
fn body_cap_from_env_parses_valid_value() {
std::env::set_var("JSS_MAX_REQUEST_BODY", "10MB");
let cap = body_cap_from_env();
assert_eq!(cap, 10_000_000, "10MB must parse to 10000000 bytes (SI)");
std::env::remove_var("JSS_MAX_REQUEST_BODY");
}
#[test]
fn body_cap_from_env_falls_back_on_invalid_value() {
std::env::set_var("JSS_MAX_REQUEST_BODY", "not-a-size");
let cap = body_cap_from_env();
std::env::remove_var("JSS_MAX_REQUEST_BODY");
assert!(
cap == DEFAULT_BODY_CAP || cap == 10_000_000,
"must fall back to default or inherit from parallel test env"
);
}
#[test]
fn nodeinfo_meta_default_values() {
let meta = NodeInfoMeta::default();
assert_eq!(meta.software_name, "solid-pod-rs-server");
assert!(
!meta.software_version.is_empty(),
"version must not be empty"
);
assert!(
!meta.open_registrations,
"default must be closed registrations"
);
assert_eq!(meta.total_users, 0);
assert_eq!(meta.base_url, "http://localhost");
}
#[test]
fn app_state_new_sets_defaults() {
std::env::remove_var("JSS_MAX_REQUEST_BODY");
let storage = Arc::new(MemoryBackend::new());
let state = AppState::new(storage);
assert_eq!(state.body_cap, DEFAULT_BODY_CAP);
assert!(state.mashlib_cdn.is_none());
assert_eq!(state.nodeinfo.software_name, "solid-pod-rs-server");
let entries = state.dotfiles.entries();
assert!(
entries.iter().any(|e| e == ".acl"),
"default dotfiles must include .acl"
);
assert!(
entries.iter().any(|e| e == ".meta"),
"default dotfiles must include .meta"
);
assert!(
entries.iter().any(|e| e == ".account"),
"default dotfiles must include .account"
);
}