Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: libs #166

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
Implement plc_resolver, add tests
sugyan committed May 3, 2024
commit 7761afa6506272a5ece1863f9e88e44aa21d6deb
4 changes: 2 additions & 2 deletions atrium-libs/src/identity/did.rs
Original file line number Diff line number Diff line change
@@ -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? {
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)]
@@ -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<_>>();
@@ -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>,
@@ -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> {
@@ -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};
@@ -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<_>>();