use std::sync::Arc;
use actix_web::http::header;
use actix_web::test;
use solid_pod_rs::auth::nip98;
use solid_pod_rs::storage::fs::FsBackend;
use solid_pod_rs_server::{build_app, AppState};
const ADMIN_KEY: &str = "test-provision-psk";
const OWNER_SK: &str = "1111111111111111111111111111111111111111111111111111111111111111";
const STRANGER_SK: &str = "2222222222222222222222222222222222222222222222222222222222222222";
fn xonly(sk_hex: &str) -> String {
let sk = k256::schnorr::SigningKey::from_bytes(&hex::decode(sk_hex).unwrap()).unwrap();
hex::encode(sk.verifying_key().to_bytes())
}
async fn fs_state() -> (AppState, tempfile::TempDir) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let fs = FsBackend::new(root.clone()).await.expect("fs backend");
let mut state = AppState::new(Arc::new(fs));
state.data_root = Some(root);
state.admin_key = Some(ADMIN_KEY.to_string());
(state, tmp)
}
fn nip98_auth(sk_hex: &str, method: &str, path: &str) -> String {
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
static BASE: OnceLock<u64> = OnceLock::new();
static SEQ: AtomicU64 = AtomicU64::new(0);
let base = *BASE.get_or_init(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.saturating_sub(30)
});
let now = base + SEQ.fetch_add(1, Ordering::Relaxed);
let url = format!("http://localhost:8080{path}");
let token = nip98::mint(&url, method, sk_hex, now)
.expect("nip98-schnorr is enabled in the workspace test build");
format!("Nostr {token}")
}
#[actix_web::test]
async fn provision_requires_admin_key() {
let (state, _tmp) = fs_state().await;
let app = test::init_service(build_app(state)).await;
let pk = xonly(OWNER_SK);
let req = test::TestRequest::post()
.uri(&format!("/_admin/provision/{pk}"))
.to_request();
let rsp = test::call_service(&app, req).await;
assert_eq!(rsp.status().as_u16(), 403, "missing admin key → 403");
let req = test::TestRequest::post()
.uri(&format!("/_admin/provision/{pk}"))
.insert_header(("x-pod-admin-key", "wrong"))
.to_request();
let rsp = test::call_service(&app, req).await;
assert_eq!(rsp.status().as_u16(), 403, "wrong admin key → 403");
}
#[actix_web::test]
async fn provision_writes_pod_into_pods_namespace_with_sibling_acl() {
let (state, tmp) = fs_state().await;
let root = tmp.path().to_path_buf();
let app = test::init_service(build_app(state)).await;
let pk = xonly(OWNER_SK);
let req = test::TestRequest::post()
.uri(&format!("/_admin/provision/{pk}"))
.insert_header(("x-pod-admin-key", ADMIN_KEY))
.to_request();
let rsp = test::call_service(&app, req).await;
assert_eq!(rsp.status().as_u16(), 200, "valid admin key → 200");
let json: serde_json::Value = test::read_body_json(rsp).await;
assert_eq!(json["ok"], true);
assert!(
json["podUrl"]
.as_str()
.unwrap()
.ends_with(&format!("/pods/{pk}/")),
"podUrl must advertise the /pods/{{pk}}/ namespace, got {:?}",
json["podUrl"]
);
assert!(
root.join("pods").join(&pk).is_dir(),
"pod dir must be data_root/pods/{{pk}} (the /pods/ URL namespace)"
);
assert!(
root.join("pods").join(format!("{pk}.acl")).is_file(),
"owner ACL must be the sibling data_root/pods/{{pk}}.acl"
);
assert!(
!root.join(&pk).exists(),
"no bare data_root/{{pk}} pod should be created (that was BUG 1)"
);
}
#[actix_web::test]
async fn provisioned_owner_can_put_into_pod() {
let (state, _tmp) = fs_state().await;
let app = test::init_service(build_app(state)).await;
let pk = xonly(OWNER_SK);
let req = test::TestRequest::post()
.uri(&format!("/_admin/provision/{pk}"))
.insert_header(("x-pod-admin-key", ADMIN_KEY))
.to_request();
assert_eq!(
test::call_service(&app, req).await.status().as_u16(),
200,
"provision must succeed"
);
let resource = format!("/pods/{pk}/media/public/x");
let auth = nip98_auth(OWNER_SK, "PUT", &resource);
let req = test::TestRequest::put()
.uri(&resource)
.insert_header((header::AUTHORIZATION, auth))
.insert_header((header::CONTENT_TYPE, "text/plain"))
.set_payload("hello")
.to_request();
let rsp = test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
201,
"provisioned owner's authenticated PUT must succeed (201 Created), \
not 403 — the resolver now finds the sibling owner ACL"
);
let auth = nip98_auth(OWNER_SK, "GET", &resource);
let req = test::TestRequest::get()
.uri(&resource)
.insert_header((header::AUTHORIZATION, auth))
.to_request();
let rsp = test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
200,
"owner GET of own resource → 200"
);
let body = test::read_body(rsp).await;
assert_eq!(&body[..], b"hello");
}
#[actix_web::test]
async fn stranger_cannot_put_into_provisioned_pod() {
let (state, _tmp) = fs_state().await;
let app = test::init_service(build_app(state)).await;
let pk = xonly(OWNER_SK);
let req = test::TestRequest::post()
.uri(&format!("/_admin/provision/{pk}"))
.insert_header(("x-pod-admin-key", ADMIN_KEY))
.to_request();
assert_eq!(test::call_service(&app, req).await.status().as_u16(), 200);
let resource = format!("/pods/{pk}/media/public/x");
let auth = nip98_auth(STRANGER_SK, "PUT", &resource);
let req = test::TestRequest::put()
.uri(&resource)
.insert_header((header::AUTHORIZATION, auth))
.insert_header((header::CONTENT_TYPE, "text/plain"))
.set_payload("intruder")
.to_request();
let rsp = test::call_service(&app, req).await;
assert_eq!(
rsp.status().as_u16(),
403,
"a non-owner's authenticated PUT must be denied (403)"
);
assert_ne!(
xonly(STRANGER_SK),
pk,
"sanity: stranger key differs from owner key"
);
}