use std::sync::Arc;
use actix_web::test;
use bytes::Bytes;
use solid_pod_rs::storage::memory::MemoryBackend;
use solid_pod_rs::storage::Storage;
use solid_pod_rs_server::{build_app, AppState};
const PREFIXES: &str = "@prefix solid: <http://www.w3.org/ns/solid/terms#>.\n\
@prefix acl: <http://www.w3.org/ns/auth/acl#>.\n";
const OPEN_ACL: &str = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n\
@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\
<#all> a acl:Authorization ; acl:agentClass foaf:Agent ;\n\
acl:accessTo </> ; acl:default </> ;\n\
acl:mode acl:Read, acl:Write, acl:Control .\n";
async fn open_storage() -> Arc<dyn Storage> {
let s: Arc<dyn Storage> = Arc::new(MemoryBackend::new());
s.put("/.acl", Bytes::from(OPEN_ACL), "text/turtle")
.await
.unwrap();
s
}
macro_rules! patch {
($app:expr, $uri:expr, $body:expr) => {{
let resp = test::call_service(
$app,
test::TestRequest::patch()
.uri($uri)
.insert_header(("content-type", "text/n3"))
.set_payload(format!(
"{PREFIXES}_:p a solid:InsertDeletePatch; {}.",
$body
))
.to_request(),
)
.await;
let status = resp.status().as_u16();
let body = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
(status, body)
}};
}
async fn stored(storage: &Arc<dyn Storage>, path: &str) -> String {
let (bytes, _) = storage.get(path).await.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[actix_web::test]
async fn deleting_a_triple_put_with_relative_iris() {
let storage = open_storage().await;
storage
.put(
"/doc.ttl",
Bytes::from("<#a> <#b> \"v1\" .\n"),
"text/turtle",
)
.await
.unwrap();
let app = test::init_service(build_app(AppState::new(storage.clone()))).await;
let (st, body) = patch!(&app, "/doc.ttl", r#"solid:deletes { <#a> <#b> "v1" }"#);
assert_eq!(st, 204, "delete of a stored relative triple: {body}");
assert!(
!stored(&storage, "/doc.ttl").await.contains("v1"),
"triple still stored"
);
}
#[actix_web::test]
async fn where_and_insert_see_stored_relative_iris() {
let storage = open_storage().await;
storage
.put(
"/doc.ttl",
Bytes::from("<#a> <#b> \"v1\" .\n"),
"text/turtle",
)
.await
.unwrap();
let app = test::init_service(build_app(AppState::new(storage.clone()))).await;
let (st, body) = patch!(
&app,
"/doc.ttl",
r#"solid:where { <#a> <#b> ?v }; solid:inserts { <#a> <#c> ?v }"#
);
assert_eq!(st, 204, "where over a stored relative triple: {body}");
let (st, body) = patch!(&app, "/doc.ttl", r#"solid:inserts { <#a> <#b> "v1" }"#);
assert_eq!(st, 204, "{body}");
let text = stored(&storage, "/doc.ttl").await;
assert_eq!(
text.matches("\"v1\"").count(),
2,
"one <#b> and one <#c> triple, no twin: {text}"
);
}
#[actix_web::test]
async fn a_second_acl_patch_can_delete_a_stored_target() {
let storage = open_storage().await;
let app = test::init_service(build_app(AppState::new(storage.clone()))).await;
let (st, body) = patch!(
&app,
"/doc.acl",
"solid:inserts { <#all> a acl:Authorization; \
acl:agentClass <http://xmlns.com/foaf/0.1/Agent>; \
acl:accessTo </doc>, </other>; acl:mode acl:Read, acl:Write, acl:Control }"
);
assert_eq!(st, 201, "create the ACL: {body}");
let text = stored(&storage, "/doc.acl").await;
assert!(
text.contains("</doc>") && text.contains("</other>"),
"{text}"
);
let (st, body) = patch!(
&app,
"/doc.acl",
"solid:deletes { <#all> acl:accessTo </other> }"
);
assert_eq!(st, 204, "second ACL patch deleting a target: {body}");
let text = stored(&storage, "/doc.acl").await;
assert!(text.contains("</doc>"), "kept target lost: {text}");
assert!(!text.contains("</other>"), "deleted target kept: {text}");
}
#[actix_web::test]
async fn patching_an_acl_with_a_condition_is_refused_not_degraded() {
let storage = open_storage().await;
let acl = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n\
@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\
<#all> a acl:Authorization ; acl:agentClass foaf:Agent ;\n\
acl:accessTo </doc> ; acl:mode acl:Read, acl:Write, acl:Control .\n\
<#paid> a acl:Authorization ; acl:agentClass foaf:Agent ;\n\
acl:accessTo </doc> ; acl:mode acl:Read ;\n\
acl:condition [ a acl:PaymentCondition ; acl:costSats 30 ] .\n";
storage
.put("/doc.acl", Bytes::from(acl), "text/turtle")
.await
.unwrap();
let app = test::init_service(build_app(AppState::new(storage.clone()))).await;
let (st, body) = patch!(
&app,
"/doc.acl",
"solid:inserts { <#friend> a acl:Authorization; \
acl:agent <https://friend.example/#me>; acl:accessTo </doc>; acl:mode acl:Read }"
);
assert_eq!(st, 409, "conditional ACL patched: {body}");
assert!(
body.contains("acl:condition"),
"refused for another reason: {body}"
);
assert_eq!(stored(&storage, "/doc.acl").await, acl, "ACL changed");
}
#[actix_web::test]
async fn an_explosive_where_clause_is_refused_promptly() {
let storage = open_storage().await;
let mut body = String::new();
for i in 0..1000 {
body.push_str(&format!("<http://e/s{i}> <http://e/p> \"{i}\" .\n"));
}
storage
.put("/big.ttl", Bytes::from(body), "text/turtle")
.await
.unwrap();
let app = test::init_service(build_app(AppState::new(storage))).await;
let started = std::time::Instant::now();
let (st, body) = patch!(
&app,
"/big.ttl",
"solid:where { ?a ?b ?c . ?d ?e ?f . ?g ?h ?i }; solid:inserts { <http://e/x> <http://e/y> ?c }"
);
assert!(
(400..500).contains(&st),
"explosive where accepted: {st} {body}"
);
assert!(
started.elapsed() < std::time::Duration::from_secs(5),
"took {:?}",
started.elapsed()
);
}
#[actix_web::test]
async fn where_and_deletes_need_read() {
let storage: Arc<dyn Storage> = Arc::new(MemoryBackend::new());
let write_only = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n\
@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\
<#w> a acl:Authorization ; acl:agentClass foaf:Agent ;\n\
acl:accessTo </> ; acl:default </> ; acl:mode acl:Write .\n";
storage
.put("/.acl", Bytes::from(write_only), "text/turtle")
.await
.unwrap();
storage
.put(
"/box.ttl",
Bytes::from("<http://e/s> <http://e/secret> \"guess\" .\n"),
"text/turtle",
)
.await
.unwrap();
let app = test::init_service(build_app(AppState::new(storage.clone()))).await;
for clause in [
r#"solid:where { <http://e/s> <http://e/secret> "guess" }; solid:inserts { <http://e/a> <http://e/b> "c" }"#,
r#"solid:deletes { <http://e/s> <http://e/secret> "guess" }"#,
] {
let (st, body) = patch!(&app, "/box.ttl", clause);
assert!(
st == 401 || st == 403,
"write-only agent inspected the resource ({st}): {clause} {body}"
);
}
let (st, body) = patch!(
&app,
"/box.ttl",
r#"solid:inserts { <http://e/a> <http://e/b> "c" }"#
);
assert_eq!(st, 204, "insert-only with Write: {body}");
assert!(stored(&storage, "/box.ttl").await.contains("guess"));
}
#[actix_web::test]
async fn json_patch_needs_read() {
let storage: Arc<dyn Storage> = Arc::new(MemoryBackend::new());
let write_only = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n\
@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\
<#w> a acl:Authorization ; acl:agentClass foaf:Agent ;\n\
acl:accessTo </> ; acl:default </> ; acl:mode acl:Write .\n";
storage
.put("/.acl", Bytes::from(write_only), "text/turtle")
.await
.unwrap();
storage
.put(
"/box.json",
Bytes::from(r#"{"secret":"guess"}"#),
"application/json",
)
.await
.unwrap();
let app = test::init_service(build_app(AppState::new(storage.clone()))).await;
let resp = test::call_service(
&app,
test::TestRequest::patch()
.uri("/box.json")
.insert_header(("content-type", "application/json-patch+json"))
.set_payload(r#"[{"op":"test","path":"/secret","value":"guess"}]"#)
.to_request(),
)
.await;
let st = resp.status().as_u16();
assert!(
st == 401 || st == 403,
"write-only JSON Patch evaluated: {st}"
);
}
#[actix_web::test]
async fn authorisation_precedes_patch_syntax_errors() {
let storage: Arc<dyn Storage> = Arc::new(MemoryBackend::new());
let app = test::init_service(build_app(AppState::new(storage))).await;
let resp = test::call_service(
&app,
test::TestRequest::patch()
.uri("/doc.ttl")
.insert_header(("content-type", "text/n3"))
.set_payload("this is not N3 {")
.to_request(),
)
.await;
let st = resp.status().as_u16();
assert!(
st == 401 || st == 403,
"syntax judged before authorisation: {st}"
);
}