#![cfg(feature = "did-nostr")]
use std::sync::Arc;
use std::time::Duration;
use serde_json::json;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use solid_pod_rs::interop::did_nostr::{
did_nostr_document, did_nostr_well_known_url, DidNostrResolver,
};
use solid_pod_rs::security::ssrf::SsrfPolicy;
const TEST_PUBKEY: &str = "abcd000000000000000000000000000000000000000000000000000000000001";
#[test]
fn did_nostr_well_known_url_format() {
let url = did_nostr_well_known_url("https://nostr.social", TEST_PUBKEY);
assert_eq!(
url,
format!("https://nostr.social/.well-known/did/nostr/{TEST_PUBKEY}.json")
);
let url2 = did_nostr_well_known_url("https://nostr.social/", TEST_PUBKEY);
assert_eq!(url, url2);
}
#[test]
fn did_nostr_document_emits_minimal_schema() {
let also = vec!["https://alice.example/me#i".to_string()];
let doc = did_nostr_document(TEST_PUBKEY, &also);
assert_eq!(doc["id"], format!("did:nostr:{TEST_PUBKEY}"));
assert_eq!(doc["alsoKnownAs"][0], "https://alice.example/me#i");
let vm = &doc["verificationMethod"][0];
assert_eq!(vm["type"], "SchnorrSecp256k1VerificationKey2019");
assert_eq!(vm["controller"], format!("did:nostr:{TEST_PUBKEY}"));
assert_eq!(vm["publicKeyHex"], TEST_PUBKEY);
assert_eq!(vm["id"], format!("did:nostr:{TEST_PUBKEY}#nostr-schnorr"));
let contexts = doc["@context"].as_array().expect("@context array");
assert!(
contexts.iter().any(|c| c == "https://w3id.org/security/suites/secp256k1-2019/v1"),
"DID Doc must include the secp256k1-2019 suite context (ADR-074 D1)",
);
}
#[tokio::test]
async fn did_nostr_resolver_returns_webid_when_backlink_present() {
let server = MockServer::start().await;
let origin = server.uri();
let web_id = format!("{origin}/alice#me");
let doc = json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": format!("did:nostr:{TEST_PUBKEY}"),
"alsoKnownAs": [web_id],
});
Mock::given(method("GET"))
.and(path(format!("/.well-known/did/nostr/{TEST_PUBKEY}.json")))
.respond_with(ResponseTemplate::new(200).set_body_json(doc))
.mount(&server)
.await;
let backlink_body = format!(
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n\
<#me> owl:sameAs <did:nostr:{TEST_PUBKEY}> .\n"
);
Mock::given(method("GET"))
.and(path("/alice"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/turtle")
.set_body_string(backlink_body),
)
.mount(&server)
.await;
let ssrf = Arc::new(SsrfPolicy::new().with_allow_loopback(true));
let resolver = DidNostrResolver::new(ssrf);
let out = resolver.resolve(&server.uri(), TEST_PUBKEY).await;
assert_eq!(
out.as_deref(),
Some(format!("{origin}/alice#me").as_str()),
"resolver must return verified WebID"
);
}
#[tokio::test]
async fn did_nostr_resolver_rejects_missing_backlink() {
let server = MockServer::start().await;
let origin = server.uri();
let web_id = format!("{origin}/bob#me");
let doc = json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": format!("did:nostr:{TEST_PUBKEY}"),
"alsoKnownAs": [web_id],
});
Mock::given(method("GET"))
.and(path(format!("/.well-known/did/nostr/{TEST_PUBKEY}.json")))
.respond_with(ResponseTemplate::new(200).set_body_json(doc))
.mount(&server)
.await;
let other_pubkey = "ffff000000000000000000000000000000000000000000000000000000000000";
let body = format!(
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n\
<#me> owl:sameAs <did:nostr:{other_pubkey}> .\n"
);
Mock::given(method("GET"))
.and(path("/bob"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/turtle")
.set_body_string(body),
)
.mount(&server)
.await;
let ssrf = Arc::new(SsrfPolicy::new().with_allow_loopback(true));
let resolver = DidNostrResolver::new(ssrf);
let out = resolver.resolve(&server.uri(), TEST_PUBKEY).await;
assert!(out.is_none(), "missing back-link must yield None");
}
#[tokio::test]
async fn did_nostr_resolver_caches_negative_result() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/.well-known/did/nostr/{TEST_PUBKEY}.json")))
.respond_with(ResponseTemplate::new(404))
.expect(1)
.mount(&server)
.await;
let ssrf = Arc::new(SsrfPolicy::new().with_allow_loopback(true));
let resolver = DidNostrResolver::new(ssrf)
.with_ttls(Duration::from_secs(300), Duration::from_secs(60));
let first = resolver.resolve(&server.uri(), TEST_PUBKEY).await;
assert!(first.is_none(), "404 must resolve to None");
let second = resolver.resolve(&server.uri(), TEST_PUBKEY).await;
assert!(second.is_none(), "cached 404 must still be None");
}
#[tokio::test]
async fn did_nostr_resolver_blocks_metadata_origin() {
let ssrf = Arc::new(SsrfPolicy::new()); let resolver = DidNostrResolver::new(ssrf);
let out = resolver
.resolve("http://169.254.169.254/", TEST_PUBKEY)
.await;
assert!(out.is_none(), "metadata origin must be blocked pre-flight");
}