mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
464 lines
12 KiB
Go
464 lines
12 KiB
Go
package auth
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/base64"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
"github.com/stretchr/testify/suite"
|
||
|
|
|
||
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||
|
|
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||
|
|
|
||
|
|
"github.com/sonr-io/sonr/client/config"
|
||
|
|
"github.com/sonr-io/sonr/client/keys"
|
||
|
|
"github.com/sonr-io/sonr/client/tx"
|
||
|
|
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||
|
|
)
|
||
|
|
|
||
|
|
// MockTxBuilder implements tx.TxBuilder for testing.
|
||
|
|
type MockTxBuilder struct {
|
||
|
|
messages []sdk.Msg
|
||
|
|
config *tx.TxConfig
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewMockTxBuilder() *MockTxBuilder {
|
||
|
|
return &MockTxBuilder{
|
||
|
|
messages: make([]sdk.Msg, 0),
|
||
|
|
config: &tx.TxConfig{
|
||
|
|
ChainID: "test-chain",
|
||
|
|
GasPrice: 0.001,
|
||
|
|
GasDenom: "usnr",
|
||
|
|
GasLimit: 200000,
|
||
|
|
GasAdjustment: 1.5,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) WithChainID(chainID string) tx.TxBuilder {
|
||
|
|
m.config.ChainID = chainID
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) WithGasPrice(price float64, denom string) tx.TxBuilder {
|
||
|
|
m.config.GasPrice = price
|
||
|
|
m.config.GasDenom = denom
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) WithGasLimit(limit uint64) tx.TxBuilder {
|
||
|
|
m.config.GasLimit = limit
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) WithMemo(memo string) tx.TxBuilder {
|
||
|
|
m.config.Memo = memo
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) WithTimeoutHeight(height uint64) tx.TxBuilder {
|
||
|
|
m.config.TimeoutHeight = height
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) AddMessage(msg sdk.Msg) tx.TxBuilder {
|
||
|
|
m.messages = append(m.messages, msg)
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) AddMessages(msgs ...sdk.Msg) tx.TxBuilder {
|
||
|
|
m.messages = append(m.messages, msgs...)
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) ClearMessages() tx.TxBuilder {
|
||
|
|
m.messages = make([]sdk.Msg, 0)
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) WithFee(amount sdk.Coins) tx.TxBuilder {
|
||
|
|
m.config.Fee = amount
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) WithGasAdjustment(adjustment float64) tx.TxBuilder {
|
||
|
|
m.config.GasAdjustment = adjustment
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) EstimateGas(ctx context.Context) (uint64, error) {
|
||
|
|
return 200000, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) Sign(ctx context.Context, keyring keys.KeyringManager) (*tx.SignedTx, error) {
|
||
|
|
unsignedTx, _ := m.Build()
|
||
|
|
return &tx.SignedTx{
|
||
|
|
UnsignedTx: unsignedTx,
|
||
|
|
Signature: []byte("mock-signature"),
|
||
|
|
PubKey: []byte("mock-pubkey"),
|
||
|
|
TxBytes: []byte("mock-tx-bytes"),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) SignAndBroadcast(ctx context.Context, keyring keys.KeyringManager) (*tx.BroadcastResult, error) {
|
||
|
|
return &tx.BroadcastResult{
|
||
|
|
TxHash: "mock-tx-hash",
|
||
|
|
Height: 12345,
|
||
|
|
Code: 0,
|
||
|
|
Log: "success",
|
||
|
|
GasUsed: 100000,
|
||
|
|
GasWanted: 200000,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) Broadcast(ctx context.Context, signedTx *tx.SignedTx) (*tx.BroadcastResult, error) {
|
||
|
|
return &tx.BroadcastResult{
|
||
|
|
TxHash: "mock-tx-hash",
|
||
|
|
Height: 12345,
|
||
|
|
Code: 0,
|
||
|
|
Log: "success",
|
||
|
|
GasUsed: 100000,
|
||
|
|
GasWanted: 200000,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) Simulate(ctx context.Context) (*tx.SimulateResult, error) {
|
||
|
|
return &tx.SimulateResult{
|
||
|
|
GasWanted: 200000,
|
||
|
|
GasUsed: 100000,
|
||
|
|
Log: "simulation success",
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) Build() (*tx.UnsignedTx, error) {
|
||
|
|
return &tx.UnsignedTx{
|
||
|
|
Messages: m.messages,
|
||
|
|
Config: m.config,
|
||
|
|
SignBytes: []byte("mock-sign-bytes"),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) BuildSigned(signature []byte, pubKey []byte) (*tx.SignedTx, error) {
|
||
|
|
unsignedTx, _ := m.Build()
|
||
|
|
return &tx.SignedTx{
|
||
|
|
UnsignedTx: unsignedTx,
|
||
|
|
Signature: signature,
|
||
|
|
PubKey: pubKey,
|
||
|
|
TxBytes: append(unsignedTx.SignBytes, signature...),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockTxBuilder) Config() *tx.TxConfig {
|
||
|
|
return m.config
|
||
|
|
}
|
||
|
|
|
||
|
|
// MockBroadcaster implements tx.Broadcaster for testing.
|
||
|
|
type MockBroadcaster struct {
|
||
|
|
broadcastedTxs [][]byte
|
||
|
|
shouldFail bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewMockBroadcaster() *MockBroadcaster {
|
||
|
|
return &MockBroadcaster{
|
||
|
|
broadcastedTxs: make([][]byte, 0),
|
||
|
|
shouldFail: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) Broadcast(ctx context.Context, txBytes []byte, mode tx.BroadcastMode) (*tx.BroadcastResult, error) {
|
||
|
|
m.broadcastedTxs = append(m.broadcastedTxs, txBytes)
|
||
|
|
|
||
|
|
if m.shouldFail {
|
||
|
|
return &tx.BroadcastResult{
|
||
|
|
Code: 1,
|
||
|
|
Log: "mock error",
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
return &tx.BroadcastResult{
|
||
|
|
TxHash: "mock-tx-hash",
|
||
|
|
Height: 12345,
|
||
|
|
Code: 0,
|
||
|
|
Log: "success",
|
||
|
|
GasUsed: 100000,
|
||
|
|
GasWanted: 200000,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) BroadcastSync(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
|
||
|
|
return m.Broadcast(ctx, txBytes, tx.BroadcastModeSync)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) BroadcastAsync(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
|
||
|
|
return m.Broadcast(ctx, txBytes, tx.BroadcastModeAsync)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) BroadcastBlock(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
|
||
|
|
return m.Broadcast(ctx, txBytes, tx.BroadcastModeBlock)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) BroadcastWithRetry(ctx context.Context, txBytes []byte, mode tx.BroadcastMode, maxRetries int) (*tx.BroadcastResult, error) {
|
||
|
|
return m.Broadcast(ctx, txBytes, mode)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) WaitForConfirmation(ctx context.Context, txHash string, timeout time.Duration) (*tx.TxConfirmation, error) {
|
||
|
|
return &tx.TxConfirmation{
|
||
|
|
TxHash: txHash,
|
||
|
|
BlockHeight: 12345,
|
||
|
|
BlockTime: time.Now(),
|
||
|
|
Code: 0,
|
||
|
|
Log: "confirmed",
|
||
|
|
GasWanted: 200000,
|
||
|
|
GasUsed: 100000,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) WithRetryConfig(config tx.RetryConfig) tx.Broadcaster {
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockBroadcaster) WithTimeout(timeout time.Duration) tx.Broadcaster {
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
// GaslessTestSuite tests gasless transaction functionality.
|
||
|
|
type GaslessTestSuite struct {
|
||
|
|
suite.Suite
|
||
|
|
manager GaslessTransactionManager
|
||
|
|
txBuilder tx.TxBuilder
|
||
|
|
broadcaster tx.Broadcaster
|
||
|
|
config *config.NetworkConfig
|
||
|
|
}
|
||
|
|
|
||
|
|
func (suite *GaslessTestSuite) SetupTest() {
|
||
|
|
cfg := config.LocalNetwork()
|
||
|
|
suite.config = &cfg
|
||
|
|
suite.txBuilder = NewMockTxBuilder()
|
||
|
|
suite.broadcaster = NewMockBroadcaster()
|
||
|
|
suite.manager = NewGaslessTransactionManager(
|
||
|
|
suite.txBuilder,
|
||
|
|
suite.broadcaster,
|
||
|
|
suite.config,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (suite *GaslessTestSuite) TestCreateGaslessRegistration() {
|
||
|
|
// Create test WebAuthn credential
|
||
|
|
credential := &WebAuthnCredential{
|
||
|
|
ID: "test-credential-id",
|
||
|
|
RawID: []byte("test-raw-id"),
|
||
|
|
PublicKey: []byte("test-public-key"),
|
||
|
|
AttestationType: "none",
|
||
|
|
Transports: []string{"usb"},
|
||
|
|
Flags: &AuthenticatorFlags{
|
||
|
|
UserPresent: true,
|
||
|
|
UserVerified: true,
|
||
|
|
},
|
||
|
|
Authenticator: &AuthenticatorData{
|
||
|
|
RPIDHash: []byte("test-rp-id-hash"),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create options
|
||
|
|
opts := &GaslessRegistrationOptions{
|
||
|
|
Username: "testuser",
|
||
|
|
AutoCreateVault: true,
|
||
|
|
WebAuthnChallenge: []byte("test-challenge"),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create gasless registration
|
||
|
|
gaslessTx, err := suite.manager.CreateGaslessRegistration(context.Background(), credential, opts)
|
||
|
|
suite.Require().NoError(err)
|
||
|
|
suite.Require().NotNil(gaslessTx)
|
||
|
|
|
||
|
|
// Verify transaction fields
|
||
|
|
suite.Equal("WebAuthn Gasless Registration", gaslessTx.Memo)
|
||
|
|
suite.Equal(uint64(200000), gaslessTx.GasLimit)
|
||
|
|
suite.Equal(signing.SignMode_SIGN_MODE_DIRECT, gaslessTx.SignMode)
|
||
|
|
suite.Len(gaslessTx.Messages, 1)
|
||
|
|
|
||
|
|
// Verify message type
|
||
|
|
msg, ok := gaslessTx.Messages[0].(*didtypes.MsgRegisterWebAuthnCredential)
|
||
|
|
suite.Require().True(ok)
|
||
|
|
suite.Equal("testuser", msg.Username)
|
||
|
|
suite.True(msg.AutoCreateVault)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (suite *GaslessTestSuite) TestBroadcastGasless() {
|
||
|
|
// Create test transaction
|
||
|
|
gaslessTx := &GaslessTransaction{
|
||
|
|
Messages: []sdk.Msg{
|
||
|
|
&didtypes.MsgRegisterWebAuthnCredential{
|
||
|
|
Controller: "sonr1xyz...",
|
||
|
|
Username: "testuser",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
Memo: "Test Gasless",
|
||
|
|
GasLimit: 200000,
|
||
|
|
SignerAddress: "sonr1xyz...",
|
||
|
|
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
|
||
|
|
TxBytes: []byte("test-tx-bytes"),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Broadcast transaction
|
||
|
|
result, err := suite.manager.BroadcastGasless(context.Background(), gaslessTx)
|
||
|
|
suite.Require().NoError(err)
|
||
|
|
suite.Require().NotNil(result)
|
||
|
|
|
||
|
|
// Verify result
|
||
|
|
suite.Equal("mock-tx-hash", result.TxHash)
|
||
|
|
suite.Equal(int64(12345), result.Height)
|
||
|
|
suite.Equal(uint32(0), result.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (suite *GaslessTestSuite) TestIsEligibleForGasless() {
|
||
|
|
// Test WebAuthn registration message
|
||
|
|
webauthnMsg := &didtypes.MsgRegisterWebAuthnCredential{
|
||
|
|
Controller: "sonr1xyz...",
|
||
|
|
Username: "testuser",
|
||
|
|
}
|
||
|
|
|
||
|
|
// Should be eligible
|
||
|
|
suite.True(suite.manager.IsEligibleForGasless([]sdk.Msg{webauthnMsg}))
|
||
|
|
|
||
|
|
// Multiple messages should not be eligible
|
||
|
|
suite.False(suite.manager.IsEligibleForGasless([]sdk.Msg{webauthnMsg, webauthnMsg}))
|
||
|
|
|
||
|
|
// Other message types should not be eligible
|
||
|
|
otherMsg := &didtypes.MsgCreateDID{
|
||
|
|
Controller: "sonr1xyz...",
|
||
|
|
}
|
||
|
|
suite.False(suite.manager.IsEligibleForGasless([]sdk.Msg{otherMsg}))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (suite *GaslessTestSuite) TestEstimateGaslessGas() {
|
||
|
|
// Test WebAuthn registration gas estimate
|
||
|
|
gas := suite.manager.EstimateGaslessGas("/did.v1.MsgRegisterWebAuthnCredential")
|
||
|
|
suite.Equal(uint64(200000), gas)
|
||
|
|
|
||
|
|
// Test default gas estimate
|
||
|
|
gas = suite.manager.EstimateGaslessGas("/unknown.message.type")
|
||
|
|
suite.Equal(uint64(100000), gas)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGaslessTestSuite(t *testing.T) {
|
||
|
|
suite.Run(t, new(GaslessTestSuite))
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestValidateGaslessEligibility tests credential validation.
|
||
|
|
func TestValidateGaslessEligibility(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
credential *WebAuthnCredential
|
||
|
|
wantError bool
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "valid credential",
|
||
|
|
credential: &WebAuthnCredential{
|
||
|
|
RawID: []byte("test-id"),
|
||
|
|
PublicKey: []byte("test-key"),
|
||
|
|
Flags: &AuthenticatorFlags{
|
||
|
|
UserPresent: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wantError: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "nil credential",
|
||
|
|
credential: nil,
|
||
|
|
wantError: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "empty credential ID",
|
||
|
|
credential: &WebAuthnCredential{
|
||
|
|
PublicKey: []byte("test-key"),
|
||
|
|
Flags: &AuthenticatorFlags{
|
||
|
|
UserPresent: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wantError: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "empty public key",
|
||
|
|
credential: &WebAuthnCredential{
|
||
|
|
RawID: []byte("test-id"),
|
||
|
|
Flags: &AuthenticatorFlags{
|
||
|
|
UserPresent: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wantError: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "no user presence",
|
||
|
|
credential: &WebAuthnCredential{
|
||
|
|
RawID: []byte("test-id"),
|
||
|
|
PublicKey: []byte("test-key"),
|
||
|
|
Flags: &AuthenticatorFlags{
|
||
|
|
UserPresent: false,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wantError: true,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
err := ValidateGaslessEligibility(tt.credential)
|
||
|
|
if tt.wantError {
|
||
|
|
require.Error(t, err)
|
||
|
|
} else {
|
||
|
|
require.NoError(t, err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestGetGaslessEndpoint tests endpoint retrieval.
|
||
|
|
func TestGetGaslessEndpoint(t *testing.T) {
|
||
|
|
cfg := &config.NetworkConfig{
|
||
|
|
RPC: "http://localhost:26657",
|
||
|
|
}
|
||
|
|
|
||
|
|
endpoint := GetGaslessEndpoint(cfg)
|
||
|
|
require.Equal(t, "http://localhost:26657", endpoint)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestWebAuthnCredentialConversion tests conversion between credential types.
|
||
|
|
func TestWebAuthnCredentialConversion(t *testing.T) {
|
||
|
|
// Create client credential
|
||
|
|
clientCred := &WebAuthnCredential{
|
||
|
|
ID: base64.RawURLEncoding.EncodeToString([]byte("test-id")),
|
||
|
|
RawID: []byte("test-id"),
|
||
|
|
PublicKey: []byte("test-public-key"),
|
||
|
|
AttestationType: "none",
|
||
|
|
Transports: []string{"usb", "nfc"},
|
||
|
|
Flags: &AuthenticatorFlags{
|
||
|
|
UserPresent: true,
|
||
|
|
UserVerified: false,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert to protobuf credential
|
||
|
|
protoCred := didtypes.WebAuthnCredential{
|
||
|
|
CredentialId: base64.RawURLEncoding.EncodeToString(clientCred.RawID),
|
||
|
|
PublicKey: clientCred.PublicKey,
|
||
|
|
AttestationType: clientCred.AttestationType,
|
||
|
|
Origin: "http://localhost",
|
||
|
|
Algorithm: -7, // ES256
|
||
|
|
CreatedAt: time.Now().Unix(),
|
||
|
|
RpId: "localhost",
|
||
|
|
RpName: "Sonr Local",
|
||
|
|
Transports: clientCred.Transports,
|
||
|
|
UserVerified: clientCred.Flags.UserVerified,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify conversion
|
||
|
|
require.Equal(t, base64.RawURLEncoding.EncodeToString([]byte("test-id")), protoCred.CredentialId)
|
||
|
|
require.Equal(t, clientCred.PublicKey, protoCred.PublicKey)
|
||
|
|
require.Equal(t, clientCred.AttestationType, protoCred.AttestationType)
|
||
|
|
require.Equal(t, clientCred.Transports, protoCred.Transports)
|
||
|
|
require.Equal(t, clientCred.Flags.UserVerified, protoCred.UserVerified)
|
||
|
|
}
|