use std::sync::Arc;
use actix_web::http::StatusCode;
use actix_web::test;
use bytes::Bytes;
use solid_pod_rs::security::DotfileAllowlist;
use solid_pod_rs::storage::memory::MemoryBackend;
use solid_pod_rs::storage::Storage;
use solid_pod_rs_server::{build_app, AppState, NodeInfoMeta};
async fn public_read_state() -> AppState {
let backend = Arc::new(MemoryBackend::new());
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:default </> ;
acl:mode acl:Read .
"#;
backend
.put("/.acl", Bytes::copy_from_slice(ttl.as_bytes()), "text/turtle")
.await
.unwrap();
AppState {
storage: backend,
dotfiles: Arc::new(DotfileAllowlist::with_defaults()),
body_cap: 16, nodeinfo: NodeInfoMeta::default(),
mashlib_cdn: None,
}
}
async fn public_write_state(body_cap: usize) -> AppState {
let backend = Arc::new(MemoryBackend::new());
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:default </> ;
acl:mode acl:Read, acl:Write, acl:Append, acl:Control .
"#;
backend
.put("/.acl", Bytes::copy_from_slice(ttl.as_bytes()), "text/turtle")
.await
.unwrap();
AppState {
storage: backend,
dotfiles: Arc::new(DotfileAllowlist::with_defaults()),
body_cap,
nodeinfo: NodeInfoMeta::default(),
mashlib_cdn: None,
}
}
#[actix_web::test]
async fn server_normalises_percent_encoded_dotdot() {
let state = public_write_state(1024).await;
let app = test::init_service(build_app(state)).await;
let req = test::TestRequest::get()
.uri("/foo/%2e%2e/escape")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"status = {:?}",
resp.status()
);
}
#[actix_web::test]
async fn server_normalises_double_percent_encoded_dotdot() {
let state = public_write_state(1024).await;
let app = test::init_service(build_app(state)).await;
let req = test::TestRequest::get()
.uri("/foo/%252e%252e/escape")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"status = {:?}",
resp.status()
);
}
#[actix_web::test]
async fn server_put_over_body_cap_returns_413() {
let state = public_write_state(16).await;
let app = test::init_service(build_app(state)).await;
let body = Bytes::from_static(&[b'A'; 32]);
let req = test::TestRequest::put()
.uri("/notes/large")
.insert_header(("content-type", "text/plain"))
.set_payload(body)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::PAYLOAD_TOO_LARGE,
"status = {:?}",
resp.status()
);
}
#[actix_web::test]
async fn server_put_under_body_cap_succeeds() {
let state = public_write_state(1024).await;
let app = test::init_service(build_app(state)).await;
let body = Bytes::from_static(b"small");
let req = test::TestRequest::put()
.uri("/notes/small")
.insert_header(("content-type", "text/plain"))
.set_payload(body)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::CREATED,
"status = {:?}",
resp.status()
);
}
#[actix_web::test]
async fn server_anonymous_put_to_protected_resource_returns_401() {
let state = public_read_state().await;
let app = test::init_service(build_app(state)).await;
let req = test::TestRequest::put()
.uri("/notes/forbidden")
.insert_header(("content-type", "text/plain"))
.set_payload(Bytes::from_static(b"x"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::UNAUTHORIZED,
"status = {:?}",
resp.status()
);
assert!(resp.headers().contains_key("wac-allow"));
}
#[actix_web::test]
async fn server_authenticated_put_with_no_acl_grant_returns_403() {
let backend = Arc::new(MemoryBackend::new());
let ttl = r#"
@prefix acl: <http://www.w3.org/ns/auth/acl#> .
<#owner> a acl:Authorization ;
acl:agent <did:nostr:owner> ;
acl:accessTo </> ;
acl:default </> ;
acl:mode acl:Read, acl:Write .
"#;
backend
.put(
"/.acl",
Bytes::copy_from_slice(ttl.as_bytes()),
"text/turtle",
)
.await
.unwrap();
let state = AppState {
storage: backend,
dotfiles: Arc::new(DotfileAllowlist::with_defaults()),
body_cap: 1024,
nodeinfo: NodeInfoMeta::default(),
mashlib_cdn: None,
};
let app = test::init_service(build_app(state)).await;
let req = test::TestRequest::put()
.uri("/notes/owner-only")
.insert_header(("content-type", "text/plain"))
.set_payload(Bytes::from_static(b"x"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::UNAUTHORIZED,
"status = {:?}",
resp.status()
);
assert!(resp.headers().contains_key("wac-allow"));
}
#[actix_web::test]
async fn server_dotfile_block_unless_allowlisted() {
let state = public_write_state(1024).await;
let app = test::init_service(build_app(state)).await;
let req = test::TestRequest::put()
.uri("/.env")
.insert_header(("content-type", "text/plain"))
.set_payload(Bytes::from_static(b"SECRET=x"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::FORBIDDEN,
"status = {:?}",
resp.status()
);
}