diff --git a/dids/did/did.go b/dids/did/did.go index cfd88a0..f2a88ae 100644 --- a/dids/did/did.go +++ b/dids/did/did.go @@ -15,10 +15,6 @@ type DID struct { // Spec: https://www.w3.org/TR/did-core/#did-syntax URI string - // URL represents the DID URI + A network location identifier for a specific resource - // Spec: https://www.w3.org/TR/did-core/#did-url-syntax - URL string - // Method specifies the DID method in the URI, which indicates the underlying // method-specific identifier scheme (e.g., jwk, dht, key, etc.). // Spec: https://www.w3.org/TR/did-core/#method-schemes @@ -48,8 +44,31 @@ type DID struct { Fragment string } +// URL represents the DID URI + A network location identifier for a specific resource +// Spec: https://www.w3.org/TR/did-core/#did-url-syntax +func (d DID) URL() string { + url := d.URI + if len(d.Params) > 0 { + var pairs []string + for key, value := range d.Params { + pairs = append(pairs, fmt.Sprintf("%s=%s", key, value)) + } + url += ";" + strings.Join(pairs, ";") + } + if len(d.Path) > 0 { + url += "/" + d.Path + } + if len(d.Query) > 0 { + url += "?" + d.Query + } + if len(d.Fragment) > 0 { + url += "#" + d.Fragment + } + return url +} + func (d DID) String() string { - return d.URL + return d.URL() } // MarshalText will convert the given DID's URL into a byte array @@ -114,7 +133,6 @@ func Parse(input string) (DID, error) { did := DID{ URI: "did:" + match[1] + ":" + match[2], - URL: input, Method: match[1], ID: match[2], } diff --git a/dids/did/did_test.go b/dids/did/did_test.go index 6919903..ec0c9ad 100644 --- a/dids/did/did_test.go +++ b/dids/did/did_test.go @@ -30,13 +30,16 @@ func TestParse(t *testing.T) { output: map[string]interface{}{ "method": "example", "id": "123456789abcdefghi", + "uri": "did:example:123456789abcdefghi", }, }, { input: "did:example:123456789abcdefghi;foo=bar;baz=qux", output: map[string]interface{}{ - "method": "example", - "id": "123456789abcdefghi", + "alternate": "did:example:123456789abcdefghi;baz=qux;foo=bar", + "method": "example", + "id": "123456789abcdefghi", + "uri": "did:example:123456789abcdefghi", "params": map[string]string{ "foo": "bar", "baz": "qux", @@ -48,6 +51,7 @@ func TestParse(t *testing.T) { output: map[string]interface{}{ "method": "example", "id": "123456789abcdefghi", + "uri": "did:example:123456789abcdefghi", "query": "foo=bar&baz=qux", }, }, @@ -56,6 +60,7 @@ func TestParse(t *testing.T) { output: map[string]interface{}{ "method": "example", "id": "123456789abcdefghi", + "uri": "did:example:123456789abcdefghi", "fragment": "keys-1", }, }, @@ -64,55 +69,70 @@ func TestParse(t *testing.T) { output: map[string]interface{}{ "method": "example", "id": "123456789abcdefghi", + "uri": "did:example:123456789abcdefghi", "query": "foo=bar&baz=qux", "fragment": "keys-1", }, }, { - input: "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1", + input: "did:example:123456789abcdefghi;foo=bar;baz=qux?p1=v1&p2=v2#keys-1", output: map[string]interface{}{ - "method": "example", - "id": "123456789abcdefghi", - "params": map[string]string{"foo": "bar", "baz": "qux"}, - "query": "foo=bar&baz=qux", - "fragment": "keys-1", + "alternate": "did:example:123456789abcdefghi;baz=quxfoo=bar;?p1=v1&p2=v2#keys-1", + "method": "example", + "id": "123456789abcdefghi", + "uri": "did:example:123456789abcdefghi", + "params": map[string]string{"foo": "bar", "baz": "qux"}, + "query": "p1=v1&p2=v2", + "fragment": "keys-1", }, }, } for _, v := range vectors { - did, err := did.Parse(v.input) + t.Run(v.input, func(t *testing.T) { + did, err := did.Parse(v.input) - if v.error && err == nil { - t.Errorf("expected error, got nil") - } + if v.error && err == nil { + t.Errorf("expected error, got nil") + } - if err != nil { - if !v.error { - t.Errorf("failed to parse did: %s", err.Error()) + if err != nil { + if !v.error { + t.Errorf("failed to parse did: %s", err.Error()) + } + return } - continue - } - assert.Equal[interface{}](t, v.output["method"], did.Method) - assert.Equal[interface{}](t, v.output["id"], did.ID) + // The Params map doesn't have a reliable order, so check both + alt, ok := v.output["alternate"] + if ok { + firstOrder := v.input == did.URL() + secondOrder := alt == did.URL() + assert.True(t, firstOrder || secondOrder, "expected one of the orders to match") + } else { + assert.Equal[interface{}](t, v.input, did.URL()) + } + assert.Equal[interface{}](t, v.output["method"], did.Method) + assert.Equal[interface{}](t, v.output["id"], did.ID) + assert.Equal[interface{}](t, v.output["uri"], did.URI) - if v.output["params"] != nil { - params, ok := v.output["params"].(map[string]string) - assert.True(t, ok, "expected params to be map[string]string") + if v.output["params"] != nil { + params, ok := v.output["params"].(map[string]string) + assert.True(t, ok, "expected params to be map[string]string") - for k, v := range params { - assert.Equal[interface{}](t, v, did.Params[k]) + for k, v := range params { + assert.Equal[interface{}](t, v, did.Params[k]) + } } - } - if v.output["query"] != nil { - assert.Equal[interface{}](t, v.output["query"], did.Query) - } + if v.output["query"] != nil { + assert.Equal[interface{}](t, v.output["query"], did.Query) + } - if v.output["fragment"] != nil { - assert.Equal[interface{}](t, v.output["fragment"], did.Fragment) - } + if v.output["fragment"] != nil { + assert.Equal[interface{}](t, v.output["fragment"], did.Fragment) + } + }) } } @@ -120,6 +140,7 @@ func TestDID_ScanValueRoundtrip(t *testing.T) { tests := []struct { object did.DID raw string + alt string wantErr bool }{ { @@ -128,6 +149,7 @@ func TestDID_ScanValueRoundtrip(t *testing.T) { }, { raw: "did:example:123456789abcdefghi;foo=bar;baz=qux", + alt: "did:example:123456789abcdefghi;baz=qux;foo=bar", object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux"), }, { @@ -144,6 +166,7 @@ func TestDID_ScanValueRoundtrip(t *testing.T) { }, { raw: "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1", + alt: "did:example:123456789abcdefghi;baz=qux;foo=bar?foo=bar&baz=qux#keys-1", object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1"), }, } @@ -159,7 +182,11 @@ func TestDID_ScanValueRoundtrip(t *testing.T) { assert.NoError(t, err) actual, ok := value.(string) assert.True(t, ok) - assert.Equal(t, tt.raw, actual) + if tt.alt != "" { + assert.True(t, actual == tt.raw || actual == tt.alt) + } else { + assert.Equal(t, tt.raw, actual) + } }) } } diff --git a/dids/diddht/diddht_test.go b/dids/diddht/diddht_test.go index 18207a4..a29b9b7 100644 --- a/dids/diddht/diddht_test.go +++ b/dids/diddht/diddht_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/tbd54566975/web5-go/dids/did" "net/http" "net/http/httptest" "testing" @@ -160,6 +161,23 @@ func TestDHTResolve(t *testing.T) { assert.EqualError(t, err, test.expectedErrorMessage) test.assertResult(t, &result.Document) + + parsedDID, err := did.Parse(test.didURI) + assert.NoError(t, err) + assert.Equal(t, test.didURI, parsedDID.URI) + + // String -> Parse roundtrip + stringParsedDID, err := did.Parse(parsedDID.String()) + assert.NoError(t, err) + assert.Equal(t, parsedDID, stringParsedDID) + + // Value -> Scan roundtrip + value, err := parsedDID.Value() + assert.NoError(t, err) + var scannedDID did.DID + err = scannedDID.Scan(value) + assert.NoError(t, err) + assert.Equal(t, parsedDID, scannedDID) }) } diff --git a/dids/didjwk/didjwk_test.go b/dids/didjwk/didjwk_test.go index 8c30221..6a460e6 100644 --- a/dids/didjwk/didjwk_test.go +++ b/dids/didjwk/didjwk_test.go @@ -2,6 +2,7 @@ package didjwk_test import ( "fmt" + "github.com/tbd54566975/web5-go/dids/did" "testing" "github.com/alecthomas/assert/v2" @@ -23,6 +24,30 @@ func TestCreate(t *testing.T) { assert.Equal(t, "did:jwk:"+did.ID, did.URI) } +func TestParse(t *testing.T) { + source, err := didjwk.Create() + assert.NoError(t, err) + original := source.DID + + // URI -> Parse + parseURIDID, err := did.Parse(original.URI) + assert.NoError(t, err) + assert.Equal(t, original, parseURIDID) + + // String -> Parse + parseStringDID, err := did.Parse(original.String()) + assert.NoError(t, err) + assert.Equal(t, original, parseStringDID) + + // Value -> Scan + var scanDID did.DID + value, err := original.Value() + assert.NoError(t, err) + err = scanDID.Scan(value) + assert.NoError(t, err) + assert.Equal(t, original, scanDID) +} + func TestResolveDIDJWK(t *testing.T) { resolver := &didjwk.Resolver{} result, err := resolver.Resolve("did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ") diff --git a/dids/didweb/didweb_test.go b/dids/didweb/didweb_test.go index 54974f5..c056b04 100644 --- a/dids/didweb/didweb_test.go +++ b/dids/didweb/didweb_test.go @@ -18,6 +18,36 @@ func TestCreate(t *testing.T) { document := bearerDID.Document assert.Equal(t, "did:web:localhost%3A8080", document.ID) assert.Equal(t, 1, len(document.VerificationMethod)) + + did := bearerDID.DID + assert.Equal(t, "web", did.Method) + assert.Equal(t, "localhost%3A8080", did.ID) + assert.Equal(t, "did:web:localhost%3A8080", did.URI) + assert.Equal(t, "did:web:localhost%3A8080", did.URL()) +} + +func TestParse(t *testing.T) { + bearerDID, err := didweb.Create("localhost:8080") + assert.NoError(t, err) + original := bearerDID.DID + + // URI -> Parse + parseURIDID, err := did.Parse(original.URI) + assert.NoError(t, err) + assert.Equal(t, original, parseURIDID) + + // String -> Parse + parseStringDID, err := did.Parse(original.String()) + assert.NoError(t, err) + assert.Equal(t, original, parseStringDID) + + // Value -> Scan + var scanDID did.DID + value, err := original.Value() + assert.NoError(t, err) + err = scanDID.Scan(value) + assert.NoError(t, err) + assert.Equal(t, original, scanDID) } func TestCreate_WithOptions(t *testing.T) { diff --git a/jws/jws.go b/jws/jws.go index 1a16ff5..2c8aad9 100644 --- a/jws/jws.go +++ b/jws/jws.go @@ -254,7 +254,7 @@ func (jws Decoded) Verify() error { return fmt.Errorf("failed to resolve DID: %w", err) } - vmSelector := didcore.ID(did.URL) + vmSelector := didcore.ID(did.URL()) verificationMethod, err := resolutionResult.Document.SelectVerificationMethod(vmSelector) if err != nil { return fmt.Errorf("kid does not match any verification method %w", err)