mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
577 lines
18 KiB
Go
577 lines
18 KiB
Go
package keeper_test
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
apiv1 "github.com/sonr-io/sonr/api/did/v1"
|
|
"github.com/sonr-io/sonr/x/did/types"
|
|
)
|
|
|
|
// TestRegisterStart tests the RegisterStart query endpoint
|
|
func (suite *QueryServerTestSuite) TestRegisterStart() {
|
|
testCases := []struct {
|
|
name string
|
|
setupFn func() *types.QueryRegisterStartRequest
|
|
expErr bool
|
|
expErrContains string
|
|
validateResp func(*types.QueryRegisterStartResponse)
|
|
}{
|
|
{
|
|
name: "success - new email assertion",
|
|
setupFn: func() *types.QueryRegisterStartRequest {
|
|
// Initialize default params for this test
|
|
err := suite.f.k.Params.Set(suite.f.ctx, types.DefaultParams())
|
|
suite.Require().NoError(err, "failed to initialize default params")
|
|
|
|
return &types.QueryRegisterStartRequest{
|
|
AssertionDid: "did:sonr:email:abc123def456",
|
|
}
|
|
},
|
|
expErr: false,
|
|
validateResp: func(resp *types.QueryRegisterStartResponse) {
|
|
suite.Require().NotEmpty(resp.Challenge, "challenge should not be empty")
|
|
suite.Require().Len(resp.Challenge, 43, "base64url-encoded 32 bytes should be 43 chars")
|
|
suite.Require().NotEmpty(resp.RelyingPartyId, "relying party ID should be set")
|
|
suite.Require().NotNil(resp.User, "user map should not be nil")
|
|
suite.Require().Equal("did:sonr:email:abc123def456", resp.User["id"])
|
|
suite.Require().Equal("Email User", resp.User["name"])
|
|
suite.Require().Contains(resp.User["displayName"], "Email")
|
|
},
|
|
},
|
|
{
|
|
name: "success - new phone assertion",
|
|
setupFn: func() *types.QueryRegisterStartRequest {
|
|
err := suite.f.k.Params.Set(suite.f.ctx, types.DefaultParams())
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryRegisterStartRequest{
|
|
AssertionDid: "did:sonr:phone:xyz789abc012",
|
|
}
|
|
},
|
|
expErr: false,
|
|
validateResp: func(resp *types.QueryRegisterStartResponse) {
|
|
suite.Require().NotEmpty(resp.Challenge)
|
|
suite.Require().NotNil(resp.User)
|
|
suite.Require().Equal("Phone User", resp.User["name"])
|
|
suite.Require().Contains(resp.User["displayName"], "Phone")
|
|
},
|
|
},
|
|
{
|
|
name: "success - github assertion",
|
|
setupFn: func() *types.QueryRegisterStartRequest {
|
|
err := suite.f.k.Params.Set(suite.f.ctx, types.DefaultParams())
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryRegisterStartRequest{
|
|
AssertionDid: "did:sonr:github:fedcba987654",
|
|
}
|
|
},
|
|
expErr: false,
|
|
validateResp: func(resp *types.QueryRegisterStartResponse) {
|
|
suite.Require().Equal("GitHub User", resp.User["name"])
|
|
suite.Require().Contains(resp.User["displayName"], "GitHub")
|
|
},
|
|
},
|
|
{
|
|
name: "error - nil request",
|
|
setupFn: func() *types.QueryRegisterStartRequest {
|
|
return nil
|
|
},
|
|
expErr: true,
|
|
expErrContains: "request cannot be nil",
|
|
},
|
|
{
|
|
name: "error - empty assertion DID",
|
|
setupFn: func() *types.QueryRegisterStartRequest {
|
|
return &types.QueryRegisterStartRequest{
|
|
AssertionDid: "",
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "assertion_did cannot be empty",
|
|
},
|
|
{
|
|
name: "error - assertion already exists",
|
|
setupFn: func() *types.QueryRegisterStartRequest {
|
|
err := suite.f.k.Params.Set(suite.f.ctx, types.DefaultParams())
|
|
suite.Require().NoError(err)
|
|
|
|
// Create an assertion first
|
|
assertionDid := "did:sonr:email:existing123"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: "did:sonr:controller123",
|
|
Subject: "test@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err = suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryRegisterStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "assertion already exists",
|
|
},
|
|
{
|
|
name: "deterministic challenge - same inputs generate same challenge",
|
|
setupFn: func() *types.QueryRegisterStartRequest {
|
|
err := suite.f.k.Params.Set(suite.f.ctx, types.DefaultParams())
|
|
suite.Require().NoError(err)
|
|
|
|
// This test verifies determinism by calling RegisterStart twice
|
|
// at the same block height with the same assertion DID
|
|
return &types.QueryRegisterStartRequest{
|
|
AssertionDid: "did:sonr:email:deterministic123",
|
|
}
|
|
},
|
|
expErr: false,
|
|
validateResp: func(resp1 *types.QueryRegisterStartResponse) {
|
|
// Call again with same params
|
|
resp2, err := suite.f.queryServer.RegisterStart(suite.f.ctx, &types.QueryRegisterStartRequest{
|
|
AssertionDid: "did:sonr:email:deterministic456",
|
|
})
|
|
suite.Require().NoError(err)
|
|
|
|
// Challenges should be different for different DIDs
|
|
suite.Require().NotEqual(
|
|
string(resp1.Challenge),
|
|
string(resp2.Challenge),
|
|
"different DIDs should produce different challenges",
|
|
)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
req := tc.setupFn()
|
|
|
|
resp, err := suite.f.queryServer.RegisterStart(suite.f.ctx, req)
|
|
|
|
if tc.expErr {
|
|
suite.Require().Error(err)
|
|
if tc.expErrContains != "" {
|
|
suite.Require().Contains(err.Error(), tc.expErrContains)
|
|
}
|
|
} else {
|
|
suite.Require().NoError(err)
|
|
suite.Require().NotNil(resp)
|
|
if tc.validateResp != nil {
|
|
tc.validateResp(resp)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLoginStart tests the LoginStart query endpoint
|
|
func (suite *QueryServerTestSuite) TestLoginStart() {
|
|
// Initialize default params for all tests
|
|
err := suite.f.k.Params.Set(suite.f.ctx, types.DefaultParams())
|
|
suite.Require().NoError(err, "failed to initialize default params")
|
|
|
|
// Setup: Create a controller DID with WebAuthn credentials
|
|
controllerDid := "did:sonr:controller789"
|
|
credId1 := "credential_id_1"
|
|
credId2 := "credential_id_2"
|
|
|
|
controllerDoc := &apiv1.DIDDocument{
|
|
Id: controllerDid,
|
|
PrimaryController: suite.f.addrs[0].String(),
|
|
VerificationMethod: []*apiv1.VerificationMethod{
|
|
{
|
|
Id: controllerDid + "#webauthn-1",
|
|
VerificationMethodKind: "WebAuthn2021",
|
|
Controller: controllerDid,
|
|
WebauthnCredential: &apiv1.WebAuthnCredential{
|
|
CredentialId: credId1,
|
|
PublicKey: []byte("test-public-key-1"),
|
|
Algorithm: -7, // ES256
|
|
},
|
|
},
|
|
{
|
|
Id: controllerDid + "#webauthn-2",
|
|
VerificationMethodKind: "WebAuthn2021",
|
|
Controller: controllerDid,
|
|
WebauthnCredential: &apiv1.WebAuthnCredential{
|
|
CredentialId: credId2,
|
|
PublicKey: []byte("test-public-key-2"),
|
|
Algorithm: -7, // ES256
|
|
},
|
|
},
|
|
{
|
|
Id: controllerDid + "#ed25519-1",
|
|
VerificationMethodKind: "Ed25519VerificationKey2020",
|
|
Controller: controllerDid,
|
|
PublicKeyMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
|
},
|
|
},
|
|
Authentication: []*apiv1.VerificationMethodReference{
|
|
{VerificationMethodId: controllerDid + "#webauthn-1"},
|
|
{VerificationMethodId: controllerDid + "#webauthn-2"},
|
|
{VerificationMethodId: controllerDid + "#ed25519-1"},
|
|
},
|
|
}
|
|
|
|
err = suite.f.k.OrmDB.DIDDocumentTable().Save(suite.f.ctx, controllerDoc)
|
|
suite.Require().NoError(err)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
setupFn func() *types.QueryLoginStartRequest
|
|
expErr bool
|
|
expErrContains string
|
|
validateResp func(*types.QueryLoginStartResponse)
|
|
}{
|
|
{
|
|
name: "success - existing assertion with WebAuthn credentials",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
assertionDid := "did:sonr:email:login123"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: controllerDid,
|
|
Subject: "user@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err := suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: false,
|
|
validateResp: func(resp *types.QueryLoginStartResponse) {
|
|
suite.Require().NotEmpty(resp.Challenge, "challenge should not be empty")
|
|
suite.Require().Len(resp.Challenge, 43, "base64url-encoded 32 bytes should be 43 chars")
|
|
suite.Require().NotEmpty(resp.RelyingPartyId, "relying party ID should be set")
|
|
suite.Require().Len(resp.CredentialIds, 2, "should extract exactly 2 WebAuthn credentials")
|
|
suite.Require().Contains(resp.CredentialIds, credId1)
|
|
suite.Require().Contains(resp.CredentialIds, credId2)
|
|
},
|
|
},
|
|
{
|
|
name: "success - embedded verification method",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
// Create controller with embedded verification method
|
|
embeddedControllerDid := "did:sonr:embedded456"
|
|
embeddedCredId := "embedded_credential_id"
|
|
|
|
embeddedDoc := &apiv1.DIDDocument{
|
|
Id: embeddedControllerDid,
|
|
PrimaryController: suite.f.addrs[0].String(),
|
|
Authentication: []*apiv1.VerificationMethodReference{
|
|
{
|
|
EmbeddedVerificationMethod: &apiv1.VerificationMethod{
|
|
Id: embeddedControllerDid + "#embedded-webauthn",
|
|
VerificationMethodKind: "WebAuthn2021",
|
|
Controller: embeddedControllerDid,
|
|
WebauthnCredential: &apiv1.WebAuthnCredential{
|
|
CredentialId: embeddedCredId,
|
|
PublicKey: []byte("embedded-key"),
|
|
Algorithm: -7,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := suite.f.k.OrmDB.DIDDocumentTable().Save(suite.f.ctx, embeddedDoc)
|
|
suite.Require().NoError(err)
|
|
|
|
assertionDid := "did:sonr:email:embedded789"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: embeddedControllerDid,
|
|
Subject: "embedded@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err = suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: false,
|
|
validateResp: func(resp *types.QueryLoginStartResponse) {
|
|
suite.Require().Len(resp.CredentialIds, 1)
|
|
suite.Require().Equal("embedded_credential_id", resp.CredentialIds[0])
|
|
},
|
|
},
|
|
{
|
|
name: "error - nil request",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
return nil
|
|
},
|
|
expErr: true,
|
|
expErrContains: "request cannot be nil",
|
|
},
|
|
{
|
|
name: "error - empty assertion DID",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: "",
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "assertion_did cannot be empty",
|
|
},
|
|
{
|
|
name: "error - assertion not found",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: "did:sonr:email:notfound999",
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "assertion DID did:sonr:email:notfound999 not found",
|
|
},
|
|
{
|
|
name: "error - assertion has no controller",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
assertionDid := "did:sonr:email:nocontroller123"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: "", // No controller
|
|
Subject: "nocontroller@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err := suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "has no controller",
|
|
},
|
|
{
|
|
name: "error - controller DID not found",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
assertionDid := "did:sonr:email:missingcontroller456"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: "did:sonr:nonexistent999",
|
|
Subject: "missing@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err := suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "controller DID did:sonr:nonexistent999 not found",
|
|
},
|
|
{
|
|
name: "error - controller DID is deactivated",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
deactivatedDid := "did:sonr:deactivated789"
|
|
deactivatedDoc := &apiv1.DIDDocument{
|
|
Id: deactivatedDid,
|
|
PrimaryController: suite.f.addrs[0].String(),
|
|
Deactivated: true, // Deactivated
|
|
}
|
|
err := suite.f.k.OrmDB.DIDDocumentTable().Save(suite.f.ctx, deactivatedDoc)
|
|
suite.Require().NoError(err)
|
|
|
|
assertionDid := "did:sonr:email:deactivatedlogin123"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: deactivatedDid,
|
|
Subject: "deactivated@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err = suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "is deactivated",
|
|
},
|
|
{
|
|
name: "error - no WebAuthn credentials found",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
noCredsControllerDid := "did:sonr:nocreds456"
|
|
noCredsDoc := &apiv1.DIDDocument{
|
|
Id: noCredsControllerDid,
|
|
PrimaryController: suite.f.addrs[0].String(),
|
|
VerificationMethod: []*apiv1.VerificationMethod{
|
|
{
|
|
Id: noCredsControllerDid + "#ed25519",
|
|
VerificationMethodKind: "Ed25519VerificationKey2020",
|
|
Controller: noCredsControllerDid,
|
|
PublicKeyMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
|
},
|
|
},
|
|
Authentication: []*apiv1.VerificationMethodReference{
|
|
{VerificationMethodId: noCredsControllerDid + "#ed25519"},
|
|
},
|
|
}
|
|
err := suite.f.k.OrmDB.DIDDocumentTable().Save(suite.f.ctx, noCredsDoc)
|
|
suite.Require().NoError(err)
|
|
|
|
assertionDid := "did:sonr:email:nocreds789"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: noCredsControllerDid,
|
|
Subject: "nocreds@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err = suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrContains: "no WebAuthn credentials found",
|
|
},
|
|
{
|
|
name: "filters out non-WebAuthn methods",
|
|
setupFn: func() *types.QueryLoginStartRequest {
|
|
mixedDid := "did:sonr:mixed123"
|
|
mixedCredId := "mixed_webauthn_cred"
|
|
|
|
mixedDoc := &apiv1.DIDDocument{
|
|
Id: mixedDid,
|
|
PrimaryController: suite.f.addrs[0].String(),
|
|
VerificationMethod: []*apiv1.VerificationMethod{
|
|
{
|
|
Id: mixedDid + "#webauthn",
|
|
VerificationMethodKind: "WebAuthn2021",
|
|
Controller: mixedDid,
|
|
WebauthnCredential: &apiv1.WebAuthnCredential{
|
|
CredentialId: mixedCredId,
|
|
PublicKey: []byte("mixed-key"),
|
|
Algorithm: -7,
|
|
},
|
|
},
|
|
{
|
|
Id: mixedDid + "#ed25519",
|
|
VerificationMethodKind: "Ed25519VerificationKey2020",
|
|
Controller: mixedDid,
|
|
PublicKeyMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
|
},
|
|
{
|
|
Id: mixedDid + "#secp256k1",
|
|
VerificationMethodKind: "EcdsaSecp256k1VerificationKey2019",
|
|
Controller: mixedDid,
|
|
PublicKeyMultibase: "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
|
|
},
|
|
},
|
|
Authentication: []*apiv1.VerificationMethodReference{
|
|
{VerificationMethodId: mixedDid + "#webauthn"},
|
|
{VerificationMethodId: mixedDid + "#ed25519"},
|
|
{VerificationMethodId: mixedDid + "#secp256k1"},
|
|
},
|
|
}
|
|
err := suite.f.k.OrmDB.DIDDocumentTable().Save(suite.f.ctx, mixedDoc)
|
|
suite.Require().NoError(err)
|
|
|
|
assertionDid := "did:sonr:email:mixed789"
|
|
assertion := &apiv1.Assertion{
|
|
Did: assertionDid,
|
|
Controller: mixedDid,
|
|
Subject: "mixed@example.com",
|
|
DidKind: "email",
|
|
}
|
|
err = suite.f.k.OrmDB.AssertionTable().Save(suite.f.ctx, assertion)
|
|
suite.Require().NoError(err)
|
|
|
|
return &types.QueryLoginStartRequest{
|
|
AssertionDid: assertionDid,
|
|
}
|
|
},
|
|
expErr: false,
|
|
validateResp: func(resp *types.QueryLoginStartResponse) {
|
|
// Should only return the WebAuthn credential, not Ed25519 or secp256k1
|
|
suite.Require().Len(resp.CredentialIds, 1, "should only extract WebAuthn credentials")
|
|
suite.Require().Equal("mixed_webauthn_cred", resp.CredentialIds[0])
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(tc.name, func() {
|
|
req := tc.setupFn()
|
|
|
|
resp, err := suite.f.queryServer.LoginStart(suite.f.ctx, req)
|
|
|
|
if tc.expErr {
|
|
suite.Require().Error(err)
|
|
if tc.expErrContains != "" {
|
|
suite.Require().Contains(err.Error(), tc.expErrContains)
|
|
}
|
|
} else {
|
|
suite.Require().NoError(err)
|
|
suite.Require().NotNil(resp)
|
|
if tc.validateResp != nil {
|
|
tc.validateResp(resp)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestUserInfoExtraction tests the extractUserInfoFromAssertionDID helper
|
|
func (suite *QueryServerTestSuite) TestUserInfoExtraction() {
|
|
// Initialize module params for RegisterStart to work
|
|
err := suite.f.k.Params.Set(suite.f.ctx, types.DefaultParams())
|
|
suite.Require().NoError(err, "failed to initialize default params")
|
|
|
|
testCases := []struct {
|
|
assertionDid string
|
|
expectedName string
|
|
expectedDispContains string
|
|
}{
|
|
{
|
|
assertionDid: "did:sonr:email:abc123def456",
|
|
expectedName: "Email User",
|
|
expectedDispContains: "Email",
|
|
},
|
|
{
|
|
assertionDid: "did:sonr:phone:xyz789abc012",
|
|
expectedName: "Phone User",
|
|
expectedDispContains: "Phone",
|
|
},
|
|
{
|
|
assertionDid: "did:sonr:tel:111222333444",
|
|
expectedName: "Phone User",
|
|
expectedDispContains: "Phone",
|
|
},
|
|
{
|
|
assertionDid: "did:sonr:github:fedcba987654",
|
|
expectedName: "GitHub User",
|
|
expectedDispContains: "GitHub",
|
|
},
|
|
{
|
|
assertionDid: "did:sonr:google:aabbccddee11",
|
|
expectedName: "Google User",
|
|
expectedDispContains: "Google",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
suite.Run(fmt.Sprintf("extract_%s", tc.expectedName), func() {
|
|
resp, err := suite.f.queryServer.RegisterStart(suite.f.ctx, &types.QueryRegisterStartRequest{
|
|
AssertionDid: tc.assertionDid,
|
|
})
|
|
|
|
suite.Require().NoError(err)
|
|
suite.Require().NotNil(resp)
|
|
suite.Require().Equal(tc.expectedName, resp.User["name"])
|
|
suite.Require().Contains(resp.User["displayName"], tc.expectedDispContains)
|
|
})
|
|
}
|
|
}
|