diff --git a/bindings/web5_uniffi_wrapper/src/dids/did.rs b/bindings/web5_uniffi_wrapper/src/dids/did.rs index aa8be9c0..da0c948b 100644 --- a/bindings/web5_uniffi_wrapper/src/dids/did.rs +++ b/bindings/web5_uniffi_wrapper/src/dids/did.rs @@ -5,7 +5,7 @@ pub struct Did(pub InnerDid); impl Did { pub fn new(uri: &str) -> Result { - let did = InnerDid::new(uri)?; + let did = InnerDid::parse(uri)?; Ok(Self(did)) } diff --git a/bindings/web5_uniffi_wrapper/src/errors.rs b/bindings/web5_uniffi_wrapper/src/errors.rs index db489676..b1581caf 100644 --- a/bindings/web5_uniffi_wrapper/src/errors.rs +++ b/bindings/web5_uniffi_wrapper/src/errors.rs @@ -8,7 +8,6 @@ use web5::crypto::dsa::DsaError; use web5::crypto::{jwk::JwkError, key_managers::KeyManagerError}; use web5::dids::bearer_did::BearerDidError; use web5::dids::data_model::DataModelError as DidDataModelError; -use web5::dids::did::DidError; use web5::dids::methods::MethodError; use web5::dids::portable_did::PortableDidError; use web5::errors::Web5Error as InnerWeb5Error; @@ -98,12 +97,6 @@ impl From for Web5Error { } } -impl From for Web5Error { - fn from(error: DidError) -> Self { - Web5Error::new(error) - } -} - impl From for Web5Error { fn from(error: PortableDidError) -> Self { Web5Error::new(error) @@ -207,4 +200,3 @@ impl From for Web5Error { } pub type Result = std::result::Result; - diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt index 178d52e5..80ea877b 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt @@ -27,7 +27,7 @@ class BearerDid { constructor(uri: String, keyManager: KeyManager) { this.rustCoreBearerDid = RustCoreBearerDid(uri, keyManager) - this.did = this.rustCoreBearerDid.getData().did + this.did = Did.fromRustCoreDidData(this.rustCoreBearerDid.getData().did) this.document = this.rustCoreBearerDid.getData().document this.keyManager = keyManager } @@ -41,7 +41,7 @@ class BearerDid { this.rustCoreBearerDid = RustCoreBearerDid.fromPortableDid(portableDid.rustCorePortableDid) val data = this.rustCoreBearerDid.getData() - this.did = data.did + this.did = Did.fromRustCoreDidData(data.did) this.document = data.document this.keyManager = data.keyManager } diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt index 1c17f125..15d03577 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt @@ -1,8 +1,39 @@ package web5.sdk.dids +import web5.sdk.rust.Did as RustCoreDid import web5.sdk.rust.DidData as RustCoreDidData /** * Representation of a [DID Core Identifier](https://www.w3.org/TR/did-core/#identifiers). */ -typealias Did = RustCoreDidData \ No newline at end of file +data class Did ( + val uri: String, + val url: String, + val method: String, + val id: String, + val params: Map? = null, + val path: String? = null, + val query: String? = null, + val fragment: String? = null +) { + companion object { + fun parse(uri: String): Did { + val rustCoreDid = RustCoreDid(uri) + val data = rustCoreDid.getData() + return Did.fromRustCoreDidData(data) + } + + internal fun fromRustCoreDidData(data: RustCoreDidData): Did { + return Did( + data.uri, + data.url, + data.method, + data.id, + data.params, + data.path, + data.query, + data.fragment, + ) + } + } +} \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index dc9c4675..af30cae1 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -29,7 +29,7 @@ class DidDht { constructor(identityKey: Jwk) { rustCoreDidDht = RustCoreDidDht.fromIdentityKey(identityKey) - this.did = rustCoreDidDht.getData().did + this.did = Did.fromRustCoreDidData(rustCoreDidDht.getData().did) this.document = rustCoreDidDht.getData().document } @@ -41,7 +41,7 @@ class DidDht { constructor(uri: String) { rustCoreDidDht = RustCoreDidDht.fromUri(uri) - this.did = rustCoreDidDht.getData().did + this.did = Did.fromRustCoreDidData(rustCoreDidDht.getData().did) this.document = rustCoreDidDht.getData().document } diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index 3bdc034f..d9292760 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -25,7 +25,7 @@ class DidJwk { constructor(publicKey: Jwk) { val rustCoreDidJwk = RustCoreDidJwk.fromPublicJwk(publicKey) - this.did = rustCoreDidJwk.getData().did + this.did = Did.fromRustCoreDidData(rustCoreDidJwk.getData().did) this.document = rustCoreDidJwk.getData().document } @@ -37,7 +37,7 @@ class DidJwk { constructor(uri: String) { val rustCoreDidJwk = RustCoreDidJwk.fromUri(uri) - this.did = rustCoreDidJwk.getData().did + this.did = Did.fromRustCoreDidData(rustCoreDidJwk.getData().did) this.document = rustCoreDidJwk.getData().document } diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index d848c194..6a86c5d2 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -28,7 +28,7 @@ class DidWeb { RustCoreDidWeb.fromUri(uri) } - this.did = rustCoreDidWeb.getData().did + this.did = Did.fromRustCoreDidData(rustCoreDidWeb.getData().did) this.document = rustCoreDidWeb.getData().document } @@ -42,7 +42,7 @@ class DidWeb { RustCoreDidWeb.fromPublicJwk(domain, publicKey); } - this.did = rustCoreDidWeb.getData().did + this.did = Did.fromRustCoreDidData(rustCoreDidWeb.getData().did) this.document = rustCoreDidWeb.getData().document } diff --git a/bound/kt/src/test/kotlin/web5/sdk/dids/DidTest.kt b/bound/kt/src/test/kotlin/web5/sdk/dids/DidTest.kt index d388892e..0c36e6a6 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/dids/DidTest.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/dids/DidTest.kt @@ -1,22 +1,249 @@ package web5.sdk.dids -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.fail +import web5.sdk.UnitTestSuite +import web5.sdk.rust.Web5Exception +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class DidTest { + private val testSuite = UnitTestSuite("did_new") + + @AfterAll + fun verifyAllTestsIncluded() { + if (testSuite.tests.isNotEmpty()) { + println("The following tests were not included or executed:") + testSuite.tests.forEach { println(it) } + fail("Not all tests were executed! ${this.testSuite.tests}") + } + } + + @Test + fun test_did_empty_string_should_error() { + this.testSuite.include() + val uri = "" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_incomplete_scheme_should_error() { + this.testSuite.include() + val uri = "did:" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_missing_id_part_should_error() { + this.testSuite.include() + val uri = "did:uport" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_missing_id_should_error() { + this.testSuite.include() + val uri = "did:uport:" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_invalid_characters_in_id_should_error() { + this.testSuite.include() + val uri = "did:uport:1234_12313***" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_invalid_bare_id_should_error() { + this.testSuite.include() + val uri = "2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_invalid_percent_encoding_should_error() { + this.testSuite.include() + val uri = "did:method:%12%1" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_invalid_percent_encoding_incomplete_should_error() { + this.testSuite.include() + val uri = "did:method:%1233%Ay" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_capitalized_method_should_error() { + this.testSuite.include() + val uri = "did:CAP:id" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_invalid_additional_id_should_error() { + this.testSuite.include() + val uri = "did:method:id::anotherid%r9" + + val exception = assertThrows { + Did.parse(uri) + } + + assertEquals("parameter error identifier regex match failure $uri", exception.msg) + } + + @Test + fun test_did_valid_did_no_params_path_query_fragment() { + this.testSuite.include() + val uri = "did:example:123456789abcdefghi" + val expected = Did( + uri = uri, + url = uri, + method = "example", + id = "123456789abcdefghi" + ) + val result = Did.parse(uri) + assertEquals(expected, result) + } + @Test - fun `test basic did creation`() { - val did = Did( - uri = "did:example:123", - url = "did:example:123#0", + fun test_did_valid_did_with_params() { + this.testSuite.include() + val uri = "did:example:123456789abcdefghi;foo=bar;baz=qux" + val expected = Did( + uri = "did:example:123456789abcdefghi", + url = uri, method = "example", - id = "123", - fragment = "fragment", - params = mapOf("foo" to "bar"), - path = "path", - query = "query" - ) - assertEquals("did:example:123#0", did.url) - } -} \ No newline at end of file + id = "123456789abcdefghi", + params = mapOf("foo" to "bar", "baz" to "qux") + ) + val result = Did.parse(uri) + assertEquals(expected, result) + } + + @Test + fun test_did_valid_did_with_query() { + this.testSuite.include() + val uri = "did:example:123456789abcdefghi?foo=bar&baz=qux" + val expected = Did( + uri = "did:example:123456789abcdefghi", + url = uri, + method = "example", + id = "123456789abcdefghi", + query = "foo=bar&baz=qux" + ) + val result = Did.parse(uri) + assertEquals(expected, result) + } + + @Test + fun test_did_valid_did_with_fragment() { + this.testSuite.include() + val uri = "did:example:123456789abcdefghi#keys-1" + val expected = Did( + uri = "did:example:123456789abcdefghi", + url = uri, + method = "example", + id = "123456789abcdefghi", + fragment = "keys-1" + ) + val result = Did.parse(uri) + assertEquals(expected, result) + } + + @Test + fun test_did_valid_did_with_query_and_fragment() { + this.testSuite.include() + val uri = "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1" + val expected = Did( + uri = "did:example:123456789abcdefghi", + url = uri, + method = "example", + id = "123456789abcdefghi", + query = "foo=bar&baz=qux", + fragment = "keys-1" + ) + val result = Did.parse(uri) + assertEquals(expected, result) + } + + @Test + fun test_did_valid_did_with_params_query_and_fragment() { + this.testSuite.include() + val uri = "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1" + val expected = Did( + uri = "did:example:123456789abcdefghi", + url = uri, + method = "example", + id = "123456789abcdefghi", + params = mapOf("foo" to "bar", "baz" to "qux"), + query = "foo=bar&baz=qux", + fragment = "keys-1" + ) + val result = Did.parse(uri) + assertEquals(expected, result) + } + + @Test + fun test_did_valid_did_with_path() { + this.testSuite.include() + val uri = "did:example:123456789abcdefghi/path/to/resource" + val expected = Did( + uri = "did:example:123456789abcdefghi", + url = uri, + method = "example", + id = "123456789abcdefghi", + path = "/path/to/resource" + ) + val result = Did.parse(uri) + assertEquals(expected, result) + } +} diff --git a/crates/web5/src/credentials/mod.rs b/crates/web5/src/credentials/mod.rs index eaa26cc6..61abdcbc 100644 --- a/crates/web5/src/credentials/mod.rs +++ b/crates/web5/src/credentials/mod.rs @@ -3,8 +3,10 @@ use std::time::SystemTimeError; use josekit::JoseError as JosekitError; use serde_json::Error as SerdeJsonError; +use crate::errors::Web5Error; + use super::dids::{ - bearer_did::BearerDidError, data_model::DataModelError, did::DidError, + bearer_did::BearerDidError, data_model::DataModelError, resolution::resolution_metadata::ResolutionMetadataError, }; @@ -38,7 +40,7 @@ pub enum CredentialError { #[error(transparent)] DidDataModel(#[from] DataModelError), #[error(transparent)] - Did(#[from] DidError), + Web5Error(#[from] Web5Error), #[error(transparent)] SystemTime(#[from] SystemTimeError), } diff --git a/crates/web5/src/credentials/verifiable_credential_1_1.rs b/crates/web5/src/credentials/verifiable_credential_1_1.rs index 7b665983..f22213a1 100644 --- a/crates/web5/src/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/credentials/verifiable_credential_1_1.rs @@ -254,7 +254,7 @@ impl VerifiableCredential { .ok_or_else(|| JosekitError::InvalidJwtFormat(CredentialError::MissingKid.into()))? .to_string(); - let did = Did::new(&kid)?; + let did = Did::parse(&kid)?; let resolution_result = ResolutionResult::new(&did.uri); if let Some(err) = resolution_result.resolution_metadata.error.clone() { diff --git a/crates/web5/src/dids/bearer_did.rs b/crates/web5/src/dids/bearer_did.rs index d2290ac2..6fe69186 100644 --- a/crates/web5/src/dids/bearer_did.rs +++ b/crates/web5/src/dids/bearer_did.rs @@ -1,23 +1,26 @@ use super::{ data_model::{document::Document, DataModelError as DidDataModelError}, - did::{Did, DidError}, + did::Did, portable_did::PortableDid, resolution::{ resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, }, }; -use crate::crypto::{ - dsa::Signer, - key_managers::{ - in_memory_key_manager::InMemoryKeyManager, key_manager::KeyManager, KeyManagerError, +use crate::{ + crypto::{ + dsa::Signer, + key_managers::{ + in_memory_key_manager::InMemoryKeyManager, key_manager::KeyManager, KeyManagerError, + }, }, + errors::Web5Error, }; use std::sync::Arc; #[derive(thiserror::Error, Debug, Clone, PartialEq)] pub enum BearerDidError { #[error(transparent)] - DidError(#[from] DidError), + Web5Error(#[from] Web5Error), #[error(transparent)] ResolutionError(#[from] ResolutionMetadataError), #[error(transparent)] @@ -45,7 +48,7 @@ impl BearerDid { Some(e) => BearerDidError::ResolutionError(e), }), Some(document) => { - let did = Did::new(uri)?; + let did = Did::parse(uri)?; Ok(Self { did, document, @@ -56,7 +59,7 @@ impl BearerDid { } pub fn from_portable_did(portable_did: PortableDid) -> Result { - let did = Did::new(&portable_did.did_uri)?; + let did = Did::parse(&portable_did.did_uri)?; let key_manager = Arc::new(InMemoryKeyManager::new()); for private_jwk in portable_did.private_jwks { diff --git a/crates/web5/src/dids/did.rs b/crates/web5/src/dids/did.rs index 2517be01..410d3f22 100644 --- a/crates/web5/src/dids/did.rs +++ b/crates/web5/src/dids/did.rs @@ -1,17 +1,7 @@ +use crate::errors::{Result, Web5Error}; use regex::Regex; -use std::collections::HashMap; use std::fmt; -use std::sync::OnceLock; - -#[derive(thiserror::Error, Debug, Clone, PartialEq)] -pub enum DidError { - #[error("Failure initializing regex pattern")] - RegexPatternFailure(String), - #[error("Failure parsing URI {0}")] - ParseFailure(String), -} - -type Result = std::result::Result; +use std::{collections::HashMap, sync::LazyLock}; #[derive(Debug, Clone, Default, PartialEq)] pub struct Did { @@ -38,42 +28,45 @@ static PATH_INDEX: usize = 6; static QUERY_INDEX: usize = 7; static FRAGMENT_INDEX: usize = 8; -static DID_URL_PATTERN: OnceLock> = OnceLock::new(); - -fn regex_pattern() -> &'static Result { - DID_URL_PATTERN.get_or_init(|| { - // relevant ABNF rules: https://www.w3.org/TR/did-core/#did-syntax - let pct_encoded_pattern: &str = r"(?:%[0-9a-fA-F]{2})"; - let method_pattern: &str = r"([a-z0-9]+)"; - let param_char_pattern: &str = r"[a-zA-Z0-9_.:%-]"; - let path_pattern: &str = r"(/[^#?]*)?"; - let query_pattern: &str = r"(\?[^\#]*)?"; - let fragment_pattern: &str = r"(\#.*)?"; - let id_char_pattern = format!(r"(?:[a-zA-Z0-9._-]|{})", pct_encoded_pattern); - let method_id_pattern = format!(r"((?:{}*:)*({}+))", id_char_pattern, id_char_pattern); - let param_pattern = format!(r";{}+={}*", param_char_pattern, param_char_pattern); - let params_pattern = format!(r"(({})*)", param_pattern); - - Regex::new(&format!( - r"^did:{}:{}{}{}{}{}$", - method_pattern, - method_id_pattern, - params_pattern, - path_pattern, - query_pattern, - fragment_pattern +static DID_URL_PATTERN: LazyLock = LazyLock::new(|| { + // relevant ABNF rules: https://www.w3.org/TR/did-core/#did-syntax + let pct_encoded_pattern: &str = r"(?:%[0-9a-fA-F]{2})"; + let method_pattern: &str = r"([a-z0-9]+)"; + let param_char_pattern: &str = r"[a-zA-Z0-9_.:%-]"; + let path_pattern: &str = r"(/[^#?]*)?"; + let query_pattern: &str = r"(\?[^\#]*)?"; + let fragment_pattern: &str = r"(\#.*)?"; + let id_char_pattern = format!(r"(?:[a-zA-Z0-9._-]|{})", pct_encoded_pattern); + let method_id_pattern = format!(r"((?:{}*:)*({}+))", id_char_pattern, id_char_pattern); + let param_pattern = format!(r";{}+={}*", param_char_pattern, param_char_pattern); + let params_pattern = format!(r"(({})*)", param_pattern); + + Regex::new(&format!( + r"^did:{}:{}{}{}{}{}$", + method_pattern, + method_id_pattern, + params_pattern, + path_pattern, + query_pattern, + fragment_pattern + )) + .map_err(|e| { + Web5Error::Parameter(format!( + "DID_URL_PATTERN regex instantiation failure: {}", + e )) - .map_err(|e| DidError::RegexPatternFailure(e.to_string())) }) -} + .unwrap() // immediately panic on startup if regex is faulty, this will assist in shift-left development +}); impl Did { - pub fn new(uri: &str) -> Result { - let pattern = regex_pattern().as_ref().map_err(|e| e.clone())?; - - let captures = pattern + pub fn parse(uri: &str) -> Result { + let captures = DID_URL_PATTERN .captures(uri) - .ok_or(DidError::ParseFailure(uri.to_string()))?; + .ok_or(Web5Error::Parameter(format!( + "identifier regex match failure {}", + uri + )))?; let params = captures .get(PARAMS_INDEX) @@ -121,130 +114,280 @@ impl Did { mod tests { use super::*; - #[test] - fn test_parse() { - let test_cases = vec![ - ("", true, None), - ("did:", true, None), - ("did:uport", true, None), - ("did:uport:", true, None), - ("did:uport:1234_12313***", true, None), - ("2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX", true, None), - ("did:method:%12%1", true, None), - ("did:method:%1233%Ay", true, None), - ("did:CAP:id", true, None), - ("did:method:id::anotherid%r9", true, None), - ( - "did:example:123456789abcdefghi", - false, - Some(Did { - uri: "did:example:123456789abcdefghi".to_string(), - url: "did:example:123456789abcdefghi".to_string(), - method: "example".to_string(), - id: "123456789abcdefghi".to_string(), - ..Default::default() - }), - ), - ( - "did:example:123456789abcdefghi;foo=bar;baz=qux", - false, - Some(Did { - uri: "did:example:123456789abcdefghi".to_string(), - url: "did:example:123456789abcdefghi;foo=bar;baz=qux".to_string(), - method: "example".to_string(), - id: "123456789abcdefghi".to_string(), - params: Some(HashMap::from([ - ("foo".to_string(), "bar".to_string()), - ("baz".to_string(), "qux".to_string()), - ])), - ..Default::default() - }), - ), - ( - "did:example:123456789abcdefghi?foo=bar&baz=qux", - false, - Some(Did { - uri: "did:example:123456789abcdefghi".to_string(), - url: "did:example:123456789abcdefghi?foo=bar&baz=qux".to_string(), - method: "example".to_string(), - id: "123456789abcdefghi".to_string(), - query: Some("foo=bar&baz=qux".to_string()), - ..Default::default() - }), - ), - ( - "did:example:123456789abcdefghi#keys-1", - false, - Some(Did { - uri: "did:example:123456789abcdefghi".to_string(), - url: "did:example:123456789abcdefghi#keys-1".to_string(), - method: "example".to_string(), - id: "123456789abcdefghi".to_string(), - fragment: Some("keys-1".to_string()), - ..Default::default() - }), - ), - ( - "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1", - false, - Some(Did { - uri: "did:example:123456789abcdefghi".to_string(), - url: "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1".to_string(), - method: "example".to_string(), - id: "123456789abcdefghi".to_string(), - query: Some("foo=bar&baz=qux".to_string()), - fragment: Some("keys-1".to_string()), - ..Default::default() - }), - ), - ( - "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1", - false, - Some(Did { - uri: "did:example:123456789abcdefghi".to_string(), - url: "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1" - .to_string(), - method: "example".to_string(), - id: "123456789abcdefghi".to_string(), - params: Some(HashMap::from([ - ("foo".to_string(), "bar".to_string()), - ("baz".to_string(), "qux".to_string()), - ])), - query: Some("foo=bar&baz=qux".to_string()), - fragment: Some("keys-1".to_string()), - ..Default::default() - }), - ), - ( - "did:example:123456789abcdefghi/path/to/resource", - false, - Some(Did { - uri: "did:example:123456789abcdefghi".to_string(), - url: "did:example:123456789abcdefghi/path/to/resource".to_string(), - method: "example".to_string(), - id: "123456789abcdefghi".to_string(), - path: Some("/path/to/resource".to_string()), - ..Default::default() - }), - ), - ]; - - for (uri, is_error, expected) in test_cases { - match Did::new(uri) { - Ok(did) => { - assert!(!is_error, "Expected error for input: {}", uri); - assert_eq!(did, expected.unwrap(), "Unexpected result for uri: {}", uri); - } - Err(e) => { - assert!(is_error, "Unexpected success for input: {}", uri); - assert_eq!( - e, - DidError::ParseFailure(uri.to_string()), - "Unexpected error result for uri: {}", - uri - ); - } - } + mod new { + use super::*; + use crate::{test_helpers::UnitTestSuite, test_name}; + + static TEST_SUITE: LazyLock = + LazyLock::new(|| UnitTestSuite::new("did_new")); + + #[test] + fn z_assert_all_suite_cases_covered() { + // fn name prefixed with `z_*` b/c rust test harness executes in alphabetical order, + // unless intentionally executed with "shuffle" https://doc.rust-lang.org/rustc/tests/index.html#--shuffle + // this may not work if shuffled or if test list grows to the extent of 100ms being insufficient wait time + + // wait 100ms to be last-in-queue of mutex lock + std::thread::sleep(std::time::Duration::from_millis(100)); + + TEST_SUITE.assert_coverage() + } + + #[test] + fn test_did_empty_string_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = ""; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_incomplete_scheme_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_missing_id_part_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:uport"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_missing_id_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:uport:"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_invalid_characters_in_id_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:uport:1234_12313***"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_invalid_bare_id_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_invalid_percent_encoding_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:method:%12%1"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_invalid_percent_encoding_incomplete_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:method:%1233%Ay"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_capitalized_method_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:CAP:id"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_invalid_additional_id_should_error() { + TEST_SUITE.include(test_name!()); + + let uri = "did:method:id::anotherid%r9"; + let result = Did::parse(uri); + assert!(result.is_err(), "Expected error for input: {}", uri); + assert_eq!( + result.unwrap_err(), + Web5Error::Parameter(format!("identifier regex match failure {}", uri)) + ); + } + + #[test] + fn test_did_valid_did_no_params_path_query_fragment() { + TEST_SUITE.include(test_name!()); + + let uri = "did:example:123456789abcdefghi"; + let expected = Did { + uri: uri.to_string(), + url: uri.to_string(), + method: "example".to_string(), + id: "123456789abcdefghi".to_string(), + ..Default::default() + }; + let result = Did::parse(uri).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_did_valid_did_with_params() { + TEST_SUITE.include(test_name!()); + + let uri = "did:example:123456789abcdefghi;foo=bar;baz=qux"; + let expected = Did { + uri: "did:example:123456789abcdefghi".to_string(), + url: uri.to_string(), + method: "example".to_string(), + id: "123456789abcdefghi".to_string(), + params: Some(HashMap::from([ + ("foo".to_string(), "bar".to_string()), + ("baz".to_string(), "qux".to_string()), + ])), + ..Default::default() + }; + let result = Did::parse(uri).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_did_valid_did_with_query() { + TEST_SUITE.include(test_name!()); + + let uri = "did:example:123456789abcdefghi?foo=bar&baz=qux"; + let expected = Did { + uri: "did:example:123456789abcdefghi".to_string(), + url: uri.to_string(), + method: "example".to_string(), + id: "123456789abcdefghi".to_string(), + query: Some("foo=bar&baz=qux".to_string()), + ..Default::default() + }; + let result = Did::parse(uri).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_did_valid_did_with_fragment() { + TEST_SUITE.include(test_name!()); + + let uri = "did:example:123456789abcdefghi#keys-1"; + let expected = Did { + uri: "did:example:123456789abcdefghi".to_string(), + url: uri.to_string(), + method: "example".to_string(), + id: "123456789abcdefghi".to_string(), + fragment: Some("keys-1".to_string()), + ..Default::default() + }; + let result = Did::parse(uri).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_did_valid_did_with_query_and_fragment() { + TEST_SUITE.include(test_name!()); + + let uri = "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1"; + let expected = Did { + uri: "did:example:123456789abcdefghi".to_string(), + url: uri.to_string(), + method: "example".to_string(), + id: "123456789abcdefghi".to_string(), + query: Some("foo=bar&baz=qux".to_string()), + fragment: Some("keys-1".to_string()), + ..Default::default() + }; + let result = Did::parse(uri).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_did_valid_did_with_params_query_and_fragment() { + TEST_SUITE.include(test_name!()); + + let uri = "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1"; + let expected = Did { + uri: "did:example:123456789abcdefghi".to_string(), + url: uri.to_string(), + method: "example".to_string(), + id: "123456789abcdefghi".to_string(), + params: Some(HashMap::from([ + ("foo".to_string(), "bar".to_string()), + ("baz".to_string(), "qux".to_string()), + ])), + query: Some("foo=bar&baz=qux".to_string()), + fragment: Some("keys-1".to_string()), + ..Default::default() + }; + let result = Did::parse(uri).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_did_valid_did_with_path() { + TEST_SUITE.include(test_name!()); + + let uri = "did:example:123456789abcdefghi/path/to/resource"; + let expected = Did { + uri: "did:example:123456789abcdefghi".to_string(), + url: uri.to_string(), + method: "example".to_string(), + id: "123456789abcdefghi".to_string(), + path: Some("/path/to/resource".to_string()), + ..Default::default() + }; + let result = Did::parse(uri).unwrap(); + assert_eq!(result, expected); } } } diff --git a/crates/web5/src/dids/methods/did_dht/mod.rs b/crates/web5/src/dids/methods/did_dht/mod.rs index 08bf8bc6..d7c55d84 100644 --- a/crates/web5/src/dids/methods/did_dht/mod.rs +++ b/crates/web5/src/dids/methods/did_dht/mod.rs @@ -86,7 +86,7 @@ impl DidDht { // } Ok(Self { - did: Did::new(&did_uri)?, + did: Did::parse(&did_uri)?, document: Document { id: did_uri.clone(), verification_method: verification_methods, @@ -107,7 +107,7 @@ impl DidDht { Some(e) => MethodError::ResolutionError(e), }), Some(document) => { - let identifer = Did::new(uri)?; + let identifer = Did::parse(uri)?; Ok(Self { did: identifer, document, @@ -119,7 +119,7 @@ impl DidDht { pub fn resolve(uri: &str) -> ResolutionResult { let result: Result = (|| { // check did method and decode id - let did = Did::new(uri).map_err(|_| ResolutionMetadataError::InvalidDid)?; + let did = Did::parse(uri).map_err(|_| ResolutionMetadataError::InvalidDid)?; if did.method != "dht" { return Ok(ResolutionResult { resolution_metadata: ResolutionMetadata { diff --git a/crates/web5/src/dids/methods/did_jwk.rs b/crates/web5/src/dids/methods/did_jwk.rs index d59e6d1e..32106cca 100644 --- a/crates/web5/src/dids/methods/did_jwk.rs +++ b/crates/web5/src/dids/methods/did_jwk.rs @@ -25,7 +25,7 @@ impl DidJwk { let uri = format!("did:jwk:{}", method_specific_id); - let did = Did::new(&uri)?; + let did = Did::parse(&uri)?; let verification_method_id = format!("{}#0", uri); @@ -58,7 +58,7 @@ impl DidJwk { Some(e) => MethodError::ResolutionError(e), }), Some(document) => { - let did = Did::new(uri)?; + let did = Did::parse(uri)?; Ok(Self { did, document }) } } @@ -66,7 +66,7 @@ impl DidJwk { pub fn resolve(uri: &str) -> ResolutionResult { let result: Result = (|| { - let did = Did::new(uri).map_err(|_| ResolutionMetadataError::InvalidDid)?; + let did = Did::parse(uri).map_err(|_| ResolutionMetadataError::InvalidDid)?; let decoded_jwk = general_purpose::URL_SAFE_NO_PAD .decode(did.id) .map_err(|_| ResolutionMetadataError::InvalidDid)?; diff --git a/crates/web5/src/dids/methods/did_web/mod.rs b/crates/web5/src/dids/methods/did_web/mod.rs index 3ec24d1f..bcdf45c9 100644 --- a/crates/web5/src/dids/methods/did_web/mod.rs +++ b/crates/web5/src/dids/methods/did_web/mod.rs @@ -85,7 +85,7 @@ impl DidWeb { }; Ok(DidWeb { - did: Did::new(&did)?, + did: Did::parse(&did)?, document, }) } @@ -98,7 +98,7 @@ impl DidWeb { Some(e) => MethodError::ResolutionError(e), }), Some(document) => { - let identifer = Did::new(uri)?; + let identifer = Did::parse(uri)?; Ok(Self { did: identifer, document, @@ -124,7 +124,7 @@ impl DidWeb { }; let result: Result = rt.block_on(async { - let did = Did::new(uri).map_err(|_| ResolutionMetadataError::InvalidDid)?; + let did = Did::parse(uri).map_err(|_| ResolutionMetadataError::InvalidDid)?; let resolution_result = Resolver::new(did).await; Ok(match resolution_result { Err(e) => ResolutionResult { diff --git a/crates/web5/src/dids/methods/did_web/resolver.rs b/crates/web5/src/dids/methods/did_web/resolver.rs index 76113b86..02965c1a 100644 --- a/crates/web5/src/dids/methods/did_web/resolver.rs +++ b/crates/web5/src/dids/methods/did_web/resolver.rs @@ -93,22 +93,22 @@ mod tests { #[tokio::test] async fn resolution_success() { let did_uri = "did:web:tbd.website"; - let result = Resolver::new(Did::new(did_uri).unwrap()); + let result = Resolver::new(Did::parse(did_uri).unwrap()); assert_eq!(result.did_url, "https://tbd.website/.well-known/did.json"); let did_uri = "did:web:tbd.website:with:path"; - let result = Resolver::new(Did::new(did_uri).unwrap()); + let result = Resolver::new(Did::parse(did_uri).unwrap()); assert_eq!(result.did_url, "https://tbd.website/with/path/did.json"); let did_uri = "did:web:tbd.website%3A8080"; - let result = Resolver::new(Did::new(did_uri).unwrap()); + let result = Resolver::new(Did::parse(did_uri).unwrap()); assert_eq!( result.did_url, "https://tbd.website:8080/.well-known/did.json" ); let did_uri = "did:web:tbd.website%3A8080:with:path"; - let result = Resolver::new(Did::new(did_uri).unwrap()); + let result = Resolver::new(Did::parse(did_uri).unwrap()); assert_eq!( result.did_url, "https://tbd.website:8080/with/path/did.json" diff --git a/crates/web5/src/dids/methods/mod.rs b/crates/web5/src/dids/methods/mod.rs index b0d56414..bad03db0 100644 --- a/crates/web5/src/dids/methods/mod.rs +++ b/crates/web5/src/dids/methods/mod.rs @@ -1,6 +1,6 @@ -use crate::crypto::dsa::DsaError; +use crate::{crypto::dsa::DsaError, errors::Web5Error}; -use super::{did::DidError, resolution::resolution_metadata::ResolutionMetadataError}; +use super::resolution::resolution_metadata::ResolutionMetadataError; use base64::DecodeError; use serde_json::Error as SerdeJsonError; @@ -12,7 +12,7 @@ pub mod did_jwk; #[derive(thiserror::Error, Debug)] pub enum MethodError { #[error(transparent)] - DidError(#[from] DidError), + Web5Error(#[from] Web5Error), #[error("Failure creating DID: {0}")] DidCreationFailure(String), #[error("Failure publishing DID: {0}")] diff --git a/crates/web5/src/dids/resolution/resolution_result.rs b/crates/web5/src/dids/resolution/resolution_result.rs index 81542bc4..bfe8200d 100644 --- a/crates/web5/src/dids/resolution/resolution_result.rs +++ b/crates/web5/src/dids/resolution/resolution_result.rs @@ -16,7 +16,7 @@ pub struct ResolutionResult { impl ResolutionResult { pub fn new(uri: &str) -> Self { - let did = match Did::new(uri) { + let did = match Did::parse(uri) { Ok(did) => did, Err(_) => { return ResolutionResult { diff --git a/crates/web5/src/lib.rs b/crates/web5/src/lib.rs index e0b15c9a..f8378f11 100644 --- a/crates/web5/src/lib.rs +++ b/crates/web5/src/lib.rs @@ -9,4 +9,4 @@ pub mod rfc3339; #[cfg(test)] mod test_helpers; #[cfg(test)] -mod test_vectors; \ No newline at end of file +mod test_vectors; diff --git a/docs/API_DESIGN.md b/docs/API_DESIGN.md index 8d1a740e..6c54bf24 100644 --- a/docs/API_DESIGN.md +++ b/docs/API_DESIGN.md @@ -332,7 +332,7 @@ CLASS Did /// Spec: https://www.w3.org/TR/did-core/#fragment. PUBLIC DATA fragment: string? - CONSTRUCTOR(uri: string) + CONSTRUCTOR parse(uri: string) ``` ### Example: Instantiate from a `did:dht` diff --git a/tests/unit_test_cases/did_new.json b/tests/unit_test_cases/did_new.json new file mode 100644 index 00000000..0fc28488 --- /dev/null +++ b/tests/unit_test_cases/did_new.json @@ -0,0 +1,19 @@ +[ + "test_did_empty_string_should_error", + "test_did_incomplete_scheme_should_error", + "test_did_missing_id_part_should_error", + "test_did_missing_id_should_error", + "test_did_invalid_characters_in_id_should_error", + "test_did_invalid_bare_id_should_error", + "test_did_invalid_percent_encoding_should_error", + "test_did_invalid_percent_encoding_incomplete_should_error", + "test_did_capitalized_method_should_error", + "test_did_invalid_additional_id_should_error", + "test_did_valid_did_no_params_path_query_fragment", + "test_did_valid_did_with_params", + "test_did_valid_did_with_query", + "test_did_valid_did_with_fragment", + "test_did_valid_did_with_query_and_fragment", + "test_did_valid_did_with_params_query_and_fragment", + "test_did_valid_did_with_path" +]