Skip to content

Commit

Permalink
Implement plc_resolver, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sugyan committed May 3, 2024
1 parent d1d114d commit 7761afa
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 44 deletions.
4 changes: 2 additions & 2 deletions atrium-libs/src/identity/did.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ use self::error::{Error, Result};
use async_trait::async_trait;

#[async_trait]
pub trait Fetcher {
pub trait Fetch {
async fn fetch(
url: &str,
timeout: Option<u64>,
) -> std::result::Result<Option<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>>;
}

#[async_trait]
pub trait Resolver {
pub trait Resolve {
async fn resolve_no_check(&self, did: &str) -> Result<Option<Vec<u8>>>;
async fn resolve_no_cache(&self, did: &str) -> Result<Option<DidDocument>> {
if let Some(got) = self.resolve_no_check(did).await? {
Expand Down
167 changes: 132 additions & 35 deletions atrium-libs/src/identity/did/did_resolver.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::error::{Error, Result};
use super::{plc_resolver::DidPlcResolver, web_resolver::DidWebResolver};
use super::{Fetcher, Resolver};
use super::{Fetch, Resolve};
use async_trait::async_trait;

#[derive(Debug)]
Expand All @@ -10,9 +10,9 @@ pub struct DidResolver<T> {
}

#[async_trait]
impl<T> Resolver for DidResolver<T>
impl<T> Resolve for DidResolver<T>
where
T: Fetcher + Send + Sync,
T: Fetch + Send + Sync,
{
async fn resolve_no_check(&self, did: &str) -> Result<Option<Vec<u8>>> {
let parts = did.split(':').collect::<Vec<_>>();
Expand Down Expand Up @@ -41,15 +41,15 @@ impl<T> Default for DidResolver<T> {
#[cfg(test)]
mod tests {
use super::*;
use crate::common_web::did_doc::{DidDocument, Service, VerificationMethod};
use mockito::{Server, ServerGuard};
use crate::common_web::did_doc::{DidDocument, Service};
use mockito::{Matcher, Server, ServerGuard};
use reqwest::{header::CONTENT_TYPE, Client};
use std::time::Duration;

struct ReqwestFetcher;

#[async_trait]
impl Fetcher for ReqwestFetcher {
impl Fetch for ReqwestFetcher {
async fn fetch(
url: &str,
timeout: Option<u64>,
Expand Down Expand Up @@ -77,55 +77,152 @@ mod tests {

fn did_doc_example() -> DidDocument {
DidDocument {
context: Some(vec![
String::from("https://www.w3.org/ns/did/v1"),
String::from("https://w3id.org/security/multikey/v1"),
String::from("https://w3id.org/security/suites/secp256k1-2019/v1"),
]),
id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"),
also_known_as: Some(vec![String::from("at://sugyan.com")]),
verification_method: Some(vec![VerificationMethod {
id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp#atproto"),
r#type: String::from("Multikey"),
controller: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"),
public_key_multibase: Some(String::from(
"zQ3shnw8ChQwGUE6gMghuvn5g7Q9YVej1MUJENqMsLmxZwRSz",
)),
}]),
context: None,
id: String::from("did:plc:234567abcdefghijklmnopqr"),
also_known_as: Some(vec![String::from("at://alice.test")]),
verification_method: None,
service: Some(vec![Service {
id: String::from("#atproto_pds"),
r#type: String::from("AtprotoPersonalDataServer"),
service_endpoint: String::from("https://puffball.us-east.host.bsky.network"),
service_endpoint: String::from("https://service.test"),
}]),
}
}

async fn server() -> ServerGuard {
async fn web_server() -> (ServerGuard, DidDocument) {
let mut did_doc = did_doc_example();
let mut server = Server::new_async().await;
did_doc.id = format!(
"did:web:{}",
urlencoding::encode(&server.host_with_port()).into_owned()
);
server
.mock("GET", "/.well-known/did.json")
.with_status(200)
.with_header(CONTENT_TYPE.as_str(), "application/did+ld+json")
.with_body(serde_json::to_vec(&did_doc).expect("failed to serialize did_doc"))
.create();
(server, did_doc)
}

async fn plc_server() -> (ServerGuard, DidDocument) {
let did_doc = did_doc_example();
let mut server = Server::new_async().await;
server
.mock(
"GET",
format!("/{}", urlencoding::encode(&did_doc.id)).as_str(),
)
.with_status(200)
.with_header(CONTENT_TYPE.as_str(), "application/did+ld+json")
.with_body(serde_json::to_vec(&did_doc_example()).expect("failed to serialize did_doc"))
.create();
server
.mock("GET", Matcher::Regex(String::from(r"^/[^/]+$")))
.with_status(404)
.create();
(server, did_doc)
}

fn resolver(plc_url: Option<String>) -> DidResolver<ReqwestFetcher> {
let timeout = Some(3000);
DidResolver {
plc: DidPlcResolver::new(
plc_url.unwrap_or(String::from("https://plc.directory")),
timeout,
),
web: DidWebResolver::new(timeout),
}
}

#[tokio::test]
async fn resolve_valid_did_web() {
let server = server().await;
let resolver = DidResolver::<ReqwestFetcher> {
plc: DidPlcResolver::new("https://plc.directory".to_string(), Some(3000)),
web: DidWebResolver::new(Some(3000)),
};
let web_did = format!(
"did:web:{}",
urlencoding::encode(&server.host_with_port()).into_owned()
);
async fn resolve_did_web_valid() {
let (_server, did_doc) = web_server().await;
let resolver = resolver(None);
let result = resolver
.ensure_resolve(&web_did, false)
.ensure_resolve(&did_doc.id, false)
.await
.expect("ensure_resolve shoud succeed with a valid did:web");
assert_eq!(result, did_doc_example());
assert_eq!(result, did_doc);
}

#[tokio::test]
async fn resolve_did_web_malformed() {
let resolver = resolver(None);

let err = resolver
.ensure_resolve("did:web:asdf", false)
.await
.expect_err("ensure_resolve should fail with a malformed did:web");
assert!(
matches!(err, Error::Fetch(_)),
"error should be Fetch: {err:?}"
);

let err = resolver
.ensure_resolve("did:web:", false)
.await
.expect_err("ensure_resolve should fail with a malformed did:web");
assert!(
matches!(err, Error::PoorlyFormattedDid(_)),
"error should be PoorlyFormattedDid: {err:?}"
);

let err = resolver
.ensure_resolve("", false)
.await
.expect_err("ensure_resolve should fail with a malformed did:web");
assert!(
matches!(err, Error::PoorlyFormattedDid(_)),
"error should be PoorlyFormattedDid: {err:?}"
);
}

#[tokio::test]
async fn resolve_did_web_with_path_components() {
let resolver = resolver(None);
let err = resolver
.ensure_resolve("did:web:example.com:u:bob", false)
.await
.expect_err("ensure_resolve should fail with did:web with path components");
assert!(
matches!(err, Error::UnsupportedDidWebPath(_)),
"error should be UnsupportedDidWebPath: {err:?}"
);
}

#[tokio::test]
async fn resolve_did_plc_valid() {
let (server, did_doc) = plc_server().await;
let resolver = resolver(Some(server.url()));
let result = resolver
.ensure_resolve(&did_doc.id, false)
.await
.expect("ensure_resolve shoud succeed with a valid did:plc");
assert_eq!(result, did_doc);
}

#[tokio::test]
async fn resolve_did_plc_malformed() {
let (server, _) = plc_server().await;
let resolver = resolver(Some(server.url()));

let err = resolver
.ensure_resolve("did:plc:asdf", false)
.await
.expect_err("ensure_resolve should fail with a malformed did:plc");
assert!(
matches!(err, Error::DidNotFound(_)),
"error should be DidNotFound: {err:?}"
);

let err = resolver
.ensure_resolve("did:plc", false)
.await
.expect_err("ensure_resolve should fail with a malformed did:plc");
assert!(
matches!(err, Error::PoorlyFormattedDid(_)),
"error should be PoorlyFormattedDid: {err:?}"
);
}
}
12 changes: 8 additions & 4 deletions atrium-libs/src/identity/did/plc_resolver.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::error::{Error, Result};
use super::{Fetcher, Resolver};
use super::{Fetch, Resolve};
use async_trait::async_trait;
use url::Url;

#[derive(Debug, Default)]
pub struct DidPlcResolver<T> {
Expand All @@ -20,11 +21,14 @@ impl<T> DidPlcResolver<T> {
}

#[async_trait]
impl<T> Resolver for DidPlcResolver<T>
impl<T> Resolve for DidPlcResolver<T>
where
T: Fetcher + Send + Sync,
T: Fetch + Send + Sync,
{
async fn resolve_no_check(&self, did: &str) -> Result<Option<Vec<u8>>> {
unimplemented!()
let url = Url::parse(&format!("{}/{}", self.plc_url, urlencoding::encode(did)))?;
T::fetch(url.as_ref(), self.timeout)
.await
.map_err(Error::Fetch)
}
}
6 changes: 3 additions & 3 deletions atrium-libs/src/identity/did/web_resolver.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::error::{Error, Result};
use super::{Fetcher, Resolver};
use super::{Fetch, Resolve};
use async_trait::async_trait;
use std::marker::PhantomData;
use url::{Host, Url};
Expand All @@ -20,9 +20,9 @@ impl<T> DidWebResolver<T> {
}

#[async_trait]
impl<T> Resolver for DidWebResolver<T>
impl<T> Resolve for DidWebResolver<T>
where
T: Fetcher + Send + Sync,
T: Fetch + Send + Sync,
{
async fn resolve_no_check(&self, did: &str) -> Result<Option<Vec<u8>>> {
let parts = did.splitn(3, ':').collect::<Vec<_>>();
Expand Down

0 comments on commit 7761afa

Please sign in to comment.