From fdda0d3ffb765ca0c353c70ea87935bc7ace616e Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 22 Mar 2026 15:31:51 +0700 Subject: [PATCH 1/6] Fix multiple bugs --- sdk/listener.go | 41 ++++++++++++++++++--------------------- utils/sort.go | 3 +-- utils/span.go | 6 +++++- x/hub/keeper/jws_token.go | 39 +++++++++++++++++++------------------ x/tier/keeper/credit.go | 2 +- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/sdk/listener.go b/sdk/listener.go index 94a26989..1bd3f332 100644 --- a/sdk/listener.go +++ b/sdk/listener.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "sync" abcitypes "github.com/cometbft/cometbft/abci/types" cometclient "github.com/cometbft/cometbft/rpc/client" @@ -25,14 +26,14 @@ type TxListener struct { cleanupFn func() } -// NewTxListener creates a new listenver from a comet client +// NewTxListener creates a new listener from a comet client func NewTxListener(client cometclient.Client) TxListener { return TxListener{ rpc: client, } } -// Event models a Cometbft Tx event with unmarsheled Msg responses +// Event models a Cometbft Tx event with unmarshaled Msg responses type Event struct { Height int64 `json:"height"` Index uint32 `json:"index"` @@ -110,11 +111,11 @@ func (l *TxListener) ListenTxs(ctx context.Context) (<-chan Event, <-chan error, return resultCh, errChn, err } -// ListenAsync spawns a go routine and listens for txs asyncrhonously, +// ListenAsync spawns a go routine and listens for txs asynchronously, // until the comet client closes the connection, the context is cancelled. // or the listener is closed. // Callback is called each time an event or an error is received -// Returns an error if connection to commet fails +// Returns an error if connection to comet fails func (l *TxListener) ListenAsync(ctx context.Context, cb func(*Event, error)) error { evs, errs, err := l.ListenTxs(ctx) if err != nil { @@ -130,9 +131,9 @@ func (l *TxListener) ListenAsync(ctx context.Context, cb func(*Event, error)) er cb(nil, err) case <-l.Done(): log.Printf("Listener closed: canceling loop") - break + return case <-ctx.Done(): - break + return } } }() @@ -155,26 +156,22 @@ func (l *TxListener) Close() { func channelMapper[T, U any](ch <-chan T, mapper func(T) (U, error)) (values <-chan U, errors <-chan error, closeFn func()) { errCh := make(chan error, mapperBuffSize) valCh := make(chan U, mapperBuffSize) - closeFn = func() { + var once sync.Once + doClose := func() { close(errCh) close(valCh) } + closeFn = func() { + once.Do(doClose) + } go func() { - for { - select { - case result, ok := <-ch: - if !ok { - close(errCh) - close(valCh) - return - } - - u, err := mapper(result) - if err != nil { - errCh <- err - } else { - valCh <- u - } + defer once.Do(doClose) + for result := range ch { + u, err := mapper(result) + if err != nil { + errCh <- err + } else { + valCh <- u } } }() diff --git a/utils/sort.go b/utils/sort.go index 81f04b71..63f41f62 100644 --- a/utils/sort.go +++ b/utils/sort.go @@ -69,7 +69,7 @@ func (s Sortable[T]) SortInPlace() { // Sort returns a sorted slice of the elements given originally func (s Sortable[T]) Sort() []T { - vals := make([]T, 0, len(s.ts)) + vals := make([]T, len(s.ts)) copy(vals, s.ts) sortable := Sortable[T]{ ts: vals, @@ -83,7 +83,6 @@ func (s Sortable[T]) Sort() []T { func SortSlice[T Ordered](elems []T) { sortable := Sortable[T]{ ts: elems, - //comparator: comparator, comparator: func(left T, right T) bool { return left < right }, } sortable.SortInPlace() diff --git a/utils/span.go b/utils/span.go index 62d5af41..6fb76a72 100644 --- a/utils/span.go +++ b/utils/span.go @@ -66,7 +66,11 @@ func WithMsgSpan(ctx sdk.Context) sdk.Context { } func GetMsgSpan(ctx sdk.Context) *MsgSpan { - return ctx.Context().Value(spanCtxKey).(*MsgSpan) + span, ok := ctx.Context().Value(spanCtxKey).(*MsgSpan) + if !ok { + return nil + } + return span } // FinalizeSpan ends the span duration frame, transforms it into an SDK Event and emits it using the event manager diff --git a/x/hub/keeper/jws_token.go b/x/hub/keeper/jws_token.go index a9acd37a..477ace8a 100644 --- a/x/hub/keeper/jws_token.go +++ b/x/hub/keeper/jws_token.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + errorsmod "cosmossdk.io/errors" "cosmossdk.io/store/prefix" storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/runtime" @@ -34,24 +35,24 @@ func (k *Keeper) jwsTokenByAccountStore(ctx context.Context) prefix.Store { // SetJWSToken stores a JWS token record and updates secondary indices. func (k *Keeper) SetJWSToken(ctx context.Context, record *types.JWSTokenRecord) error { if record == nil { - return fmt.Errorf("JWS token record cannot be nil") + return errorsmod.Wrap(types.ErrInvalidInput, "JWS token record cannot be nil") } if record.TokenHash == "" { - return fmt.Errorf("token hash cannot be empty") + return errorsmod.Wrap(types.ErrInvalidInput, "token hash cannot be empty") } if record.IssuerDid == "" { - return fmt.Errorf("issuer DID cannot be empty") + return errorsmod.Wrap(types.ErrInvalidInput, "issuer DID cannot be empty") } // Validate authorized account if provided if record.AuthorizedAccount != "" { if _, err := sdk.AccAddressFromBech32(record.AuthorizedAccount); err != nil { - return fmt.Errorf("invalid authorized account address: %w", err) + return errorsmod.Wrap(types.ErrInvalidInput, "invalid authorized account address: "+err.Error()) } } else if !k.GetChainConfig(ctx).IgnoreBearerAuth { - return fmt.Errorf("authorized account is required when bearer auth is enabled") + return errorsmod.Wrap(types.ErrInvalidInput, "authorized account is required when bearer auth is enabled") } bz, err := k.cdc.Marshal(record) @@ -104,7 +105,7 @@ func (k *Keeper) DeleteJWSToken(ctx context.Context, tokenHash string) error { // First, get the record to access DID and account for index cleanup record, found := k.GetJWSToken(ctx, tokenHash) if !found { - return fmt.Errorf("JWS token not found: %s", tokenHash) + return errorsmod.Wrap(types.ErrJWSTokenNotFound, tokenHash) } // Delete from primary store @@ -217,19 +218,19 @@ func (k *Keeper) GetAllJWSTokens(ctx context.Context) ([]*types.JWSTokenRecord, // UpdateJWSTokenStatus updates the status of a JWS token. func (k *Keeper) UpdateJWSTokenStatus(ctx context.Context, tokenHash string, status types.JWSTokenStatus, invalidatedBy string) error { if tokenHash == "" { - return fmt.Errorf("token hash cannot be empty") + return errorsmod.Wrap(types.ErrInvalidInput, "token hash cannot be empty") } record, found := k.GetJWSToken(ctx, tokenHash) if !found { - return fmt.Errorf("JWS token not found: %s", tokenHash) + return errorsmod.Wrap(types.ErrJWSTokenNotFound, tokenHash) } record.Status = status if status == types.JWSTokenStatus_STATUS_INVALID { - now := time.Now() - record.InvalidatedAt = &now + blockTime := sdk.UnwrapSDKContext(ctx).BlockTime() + record.InvalidatedAt = &blockTime if invalidatedBy != "" { record.InvalidatedBy = invalidatedBy } @@ -241,19 +242,19 @@ func (k *Keeper) UpdateJWSTokenStatus(ctx context.Context, tokenHash string, sta // RecordJWSTokenUsage updates the last used timestamp for a JWS token. func (k *Keeper) RecordJWSTokenUsage(ctx context.Context, tokenHash string) error { if tokenHash == "" { - return fmt.Errorf("token hash cannot be empty") + return errorsmod.Wrap(types.ErrInvalidInput, "token hash cannot be empty") } record, found := k.GetJWSToken(ctx, tokenHash) if !found { - return fmt.Errorf("JWS token not found: %s", tokenHash) + return errorsmod.Wrap(types.ErrJWSTokenNotFound, tokenHash) } - now := time.Now() + blockTime := sdk.UnwrapSDKContext(ctx).BlockTime() if record.FirstUsedAt == nil { - record.FirstUsedAt = &now + record.FirstUsedAt = &blockTime } - record.LastUsedAt = &now + record.LastUsedAt = &blockTime return k.SetJWSToken(ctx, record) } @@ -302,11 +303,11 @@ func (k *Keeper) StoreOrUpdateJWSToken( // Validate that new tokens aren't already expired if !expiresAt.IsZero() && expiresAt.Before(sdk.UnwrapSDKContext(ctx).BlockTime()) { - return fmt.Errorf("cannot create token with expiration time in the past") + return errorsmod.Wrap(types.ErrJWSTokenExpired, "cannot create token with expiration time in the past") } // Create new token record - now := time.Now() + blockTime := sdk.UnwrapSDKContext(ctx).BlockTime() record := &types.JWSTokenRecord{ TokenHash: tokenHash, BearerToken: bearerToken, @@ -315,8 +316,8 @@ func (k *Keeper) StoreOrUpdateJWSToken( IssuedAt: issuedAt, ExpiresAt: expiresAt, Status: types.JWSTokenStatus_STATUS_VALID, - FirstUsedAt: &now, - LastUsedAt: &now, + FirstUsedAt: &blockTime, + LastUsedAt: &blockTime, } return k.SetJWSToken(ctx, record) diff --git a/x/tier/keeper/credit.go b/x/tier/keeper/credit.go index 182e231f..52245a8d 100644 --- a/x/tier/keeper/credit.go +++ b/x/tier/keeper/credit.go @@ -212,7 +212,7 @@ func (k *Keeper) resetAllCredits(ctx context.Context, epochNumber int64) (err er if err != nil { return errorsmod.Wrapf(err, "mint %s ucredit to %s", credit, delAddr) } - totalCredit.Add(credit) + totalCredit = totalCredit.Add(credit) } // Set total credit amount From 98ee6fb6707c4ab47ea5d4b6e645b53ea146cce1 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 22 Mar 2026 15:32:26 +0700 Subject: [PATCH 2/6] Add tests --- x/acp/capability/manager_test.go | 152 ++++++++++++++++++++++++++ x/acp/capability/types_test.go | 22 ++++ x/acp/did/did_test.go | 23 ++++ x/acp/did/types_test.go | 57 ++++++++++ x/acp/metrics/metrics_test.go | 17 +++ x/acp/simulation/simulation_test.go | 137 +++++++++++++++++++++++ x/acp/stores/kv_stores_cosmos_test.go | 100 +++++++++++++++++ x/acp/stores/marshaler_test.go | 33 ++++++ x/acp/types/access_decision_test.go | 91 +++++++++++++++ x/acp/types/codec_test.go | 15 +++ x/acp/types/commitment_test.go | 62 +++++++++++ x/acp/types/constants_test.go | 18 +++ x/acp/types/errors_test.go | 45 ++++++++ x/acp/types/keys_test.go | 30 +++++ x/acp/types/mapper_test.go | 88 +++++++++++++++ x/acp/types/messages_test.go | 105 ++++++++++++++++++ x/acp/types/params_test.go | 41 +++++++ x/acp/types/policy_cmd_test.go | 82 ++++++++++++++ x/acp/types/time_test.go | 108 ++++++++++++++++++ x/acp/utils/utils_test.go | 81 ++++++++++++++ 20 files changed, 1307 insertions(+) create mode 100644 x/acp/capability/manager_test.go create mode 100644 x/acp/capability/types_test.go create mode 100644 x/acp/did/did_test.go create mode 100644 x/acp/did/types_test.go create mode 100644 x/acp/metrics/metrics_test.go create mode 100644 x/acp/simulation/simulation_test.go create mode 100644 x/acp/stores/kv_stores_cosmos_test.go create mode 100644 x/acp/stores/marshaler_test.go create mode 100644 x/acp/types/access_decision_test.go create mode 100644 x/acp/types/codec_test.go create mode 100644 x/acp/types/commitment_test.go create mode 100644 x/acp/types/constants_test.go create mode 100644 x/acp/types/errors_test.go create mode 100644 x/acp/types/keys_test.go create mode 100644 x/acp/types/mapper_test.go create mode 100644 x/acp/types/messages_test.go create mode 100644 x/acp/types/params_test.go create mode 100644 x/acp/types/policy_cmd_test.go create mode 100644 x/acp/types/time_test.go create mode 100644 x/acp/utils/utils_test.go diff --git a/x/acp/capability/manager_test.go b/x/acp/capability/manager_test.go new file mode 100644 index 00000000..bc873dc9 --- /dev/null +++ b/x/acp/capability/manager_test.go @@ -0,0 +1,152 @@ +package capability + +import ( + "testing" + + "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitykeeper "github.com/cosmos/ibc-go/modules/capability/keeper" + "github.com/stretchr/testify/require" +) + +func setupCapKeeper(t *testing.T) (sdk.Context, *capabilitykeeper.ScopedKeeper, *capabilitykeeper.ScopedKeeper) { + capStoreKey := storetypes.NewKVStoreKey("capkeeper") + capMemStoreKey := storetypes.NewKVStoreKey("capkeepermem") + + db := dbm.NewMemDB() + stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + stateStore.MountStoreWithDB(capStoreKey, storetypes.StoreTypeDB, db) + stateStore.MountStoreWithDB(capMemStoreKey, storetypes.StoreTypeDB, db) + require.NoError(t, stateStore.LoadLatestVersion()) + + registry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(registry) + capKeeper := capabilitykeeper.NewKeeper(cdc, capStoreKey, capMemStoreKey) + + acpScoped := capKeeper.ScopeToModule("acp") + otherScoped := capKeeper.ScopeToModule("other") + + ctx := sdk.NewContext(stateStore, cmtproto.Header{}, false, log.NewNopLogger()) + capKeeper.Seal() + + return ctx, &acpScoped, &otherScoped +} + +func TestNewPolicyCapabilityManager(t *testing.T) { + _, acpScoped, _ := setupCapKeeper(t) + mgr := NewPolicyCapabilityManager(acpScoped) + require.NotNil(t, mgr) +} + +func TestIssueAndFetch(t *testing.T) { + ctx, acpScoped, _ := setupCapKeeper(t) + mgr := NewPolicyCapabilityManager(acpScoped) + + cap, err := mgr.Issue(ctx, "policy-1") + require.NoError(t, err) + require.NotNil(t, cap) + require.Equal(t, "policy-1", cap.GetPolicyId()) + require.NotNil(t, cap.GetCosmosCapability()) + + fetched, err := mgr.Fetch(ctx, "policy-1") + require.NoError(t, err) + require.NotNil(t, fetched) + require.Equal(t, "policy-1", fetched.GetPolicyId()) +} + +func TestFetchNonExistent(t *testing.T) { + ctx, acpScoped, _ := setupCapKeeper(t) + mgr := NewPolicyCapabilityManager(acpScoped) + + _, err := mgr.Fetch(ctx, "nonexistent") + require.Error(t, err) +} + +func TestClaimCapability(t *testing.T) { + ctx, acpScoped, otherScoped := setupCapKeeper(t) + acpMgr := NewPolicyCapabilityManager(acpScoped) + otherMgr := NewPolicyCapabilityManager(otherScoped) + + // acp issues + cap, err := acpMgr.Issue(ctx, "policy-1") + require.NoError(t, err) + + // other module claims + err = otherMgr.Claim(ctx, cap) + require.NoError(t, err) + + // other module can now fetch + fetched, err := otherMgr.Fetch(ctx, "policy-1") + require.NoError(t, err) + require.NotNil(t, fetched) +} + +func TestValidateCapability(t *testing.T) { + ctx, acpScoped, otherScoped := setupCapKeeper(t) + acpMgr := NewPolicyCapabilityManager(acpScoped) + otherMgr := NewPolicyCapabilityManager(otherScoped) + + cap, err := acpMgr.Issue(ctx, "policy-1") + require.NoError(t, err) + + err = otherMgr.Claim(ctx, cap) + require.NoError(t, err) + + // validate from the other module's perspective + err = otherMgr.Validate(ctx, cap) + // Filter removes "acp", leaving ["other"]. len > 0 => returns true. + // So Validate passes, but for the wrong reason. + require.NoError(t, err) +} + +func TestGetOwnerModule(t *testing.T) { + ctx, acpScoped, otherScoped := setupCapKeeper(t) + acpMgr := NewPolicyCapabilityManager(acpScoped) + otherMgr := NewPolicyCapabilityManager(otherScoped) + + cap, err := acpMgr.Issue(ctx, "policy-1") + require.NoError(t, err) + + err = otherMgr.Claim(ctx, cap) + require.NoError(t, err) + + owner, err := otherMgr.GetOwnerModule(ctx, cap) + require.NoError(t, err) + require.Equal(t, "other", owner) +} + +func TestGetOwnerModuleNoClaimer(t *testing.T) { + ctx, acpScoped, _ := setupCapKeeper(t) + acpMgr := NewPolicyCapabilityManager(acpScoped) + + cap, err := acpMgr.Issue(ctx, "policy-1") + require.NoError(t, err) + + // only acp owns it, after filtering acp out, mods is empty + _, err = acpMgr.GetOwnerModule(ctx, cap) + require.Error(t, err) +} + +func TestIsOwnedByAcpModuleInvertedLogic(t *testing.T) { + // When only acp owns the capability, the filter removes "acp", + // leaving an empty list, so isOwnedByAcpModule returns false. + // This means Validate would fail for a legitimately issued capability + // if no other module has claimed it yet. + ctx, acpScoped, _ := setupCapKeeper(t) + acpMgr := NewPolicyCapabilityManager(acpScoped) + + cap, err := acpMgr.Issue(ctx, "policy-1") + require.NoError(t, err) + + // Validate should pass since acp issued it, but due to the inverted logic, + // it returns ErrInvalidCapability + err = acpMgr.Validate(ctx, cap) + require.Error(t, err, "isOwnedByAcpModule incorrectly returns false when only acp owns capability") +} diff --git a/x/acp/capability/types_test.go b/x/acp/capability/types_test.go new file mode 100644 index 00000000..e9f9d4fc --- /dev/null +++ b/x/acp/capability/types_test.go @@ -0,0 +1,22 @@ +package capability + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPolicyCapabilityGetCapabilityName(t *testing.T) { + cap := &PolicyCapability{policyId: "test-policy-123"} + require.Equal(t, "/acp/module_policies/test-policy-123", cap.GetCapabilityName()) +} + +func TestPolicyCapabilityGetPolicyId(t *testing.T) { + cap := &PolicyCapability{policyId: "test-policy-123"} + require.Equal(t, "test-policy-123", cap.GetPolicyId()) +} + +func TestPolicyCapabilityGetCosmosCapability(t *testing.T) { + cap := &PolicyCapability{policyId: "p1", capability: nil} + require.Nil(t, cap.GetCosmosCapability()) +} diff --git a/x/acp/did/did_test.go b/x/acp/did/did_test.go new file mode 100644 index 00000000..0cdd3b82 --- /dev/null +++ b/x/acp/did/did_test.go @@ -0,0 +1,23 @@ +package did + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestProduceDID(t *testing.T) { + did, signer, err := ProduceDID() + require.NoError(t, err) + require.NotEmpty(t, did) + require.NotNil(t, signer) + require.Contains(t, did, "did:key:") +} + +func TestProduceDIDUniqueness(t *testing.T) { + did1, _, err := ProduceDID() + require.NoError(t, err) + did2, _, err := ProduceDID() + require.NoError(t, err) + require.NotEqual(t, did1, did2) +} diff --git a/x/acp/did/types_test.go b/x/acp/did/types_test.go new file mode 100644 index 00000000..37e334bd --- /dev/null +++ b/x/acp/did/types_test.go @@ -0,0 +1,57 @@ +package did + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/stretchr/testify/require" +) + +func TestIsValidDID(t *testing.T) { + tests := []struct { + name string + did string + wantErr bool + }{ + {"valid did:key", "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", false}, + {"invalid did", "not-a-did", true}, + {"empty string", "", true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := IsValidDID(tc.did) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestIssueDID(t *testing.T) { + priv := ed25519.GenPrivKey() + pub := priv.PubKey() + acc := authtypes.NewBaseAccount(pub.Address().Bytes(), pub, 0, 0) + + did, err := IssueDID(acc) + require.NoError(t, err) + require.Contains(t, did, "did:key:") +} + +func TestDIDFromPubKeyNilPanics(t *testing.T) { + require.Panics(t, func() { + DIDFromPubKey(nil) + }) +} + +func TestIssueModuleDID(t *testing.T) { + did := IssueModuleDID("acp") + require.Equal(t, "did:module:acp", did) +} + +func TestIssueInterchainAccountDID(t *testing.T) { + did := IssueInterchainAccountDID("cosmos1abc") + require.Equal(t, "did:ica:cosmos1abc", did) +} diff --git a/x/acp/metrics/metrics_test.go b/x/acp/metrics/metrics_test.go new file mode 100644 index 00000000..2e4c4394 --- /dev/null +++ b/x/acp/metrics/metrics_test.go @@ -0,0 +1,17 @@ +package metrics + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMetricConstants(t *testing.T) { + require.Equal(t, "sourcehub_acp_msg_total", MsgTotal) + require.Equal(t, "sourcehub_acp_msg_errors_total", MsgErrors) + require.Equal(t, "sourcehub_acp_msg_seconds", MsgSeconds) + require.Equal(t, "sourcehub_acp_invariant_violation_total", InvariantViolation) + require.Equal(t, "sourcehub_acp_query_total", QueryTotal) + require.Equal(t, "sourcehub_acp_query_errors_total", QueryErrors) + require.Equal(t, "sourcehub_acp_query_seconds", QuerySeconds) +} diff --git a/x/acp/simulation/simulation_test.go b/x/acp/simulation/simulation_test.go new file mode 100644 index 00000000..3f127712 --- /dev/null +++ b/x/acp/simulation/simulation_test.go @@ -0,0 +1,137 @@ +package simulation + +import ( + "math/rand" + "testing" + + "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/runtime" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + capabilitykeeper "github.com/cosmos/ibc-go/modules/capability/keeper" + "github.com/stretchr/testify/require" + + "github.com/sourcenetwork/sourcehub/x/acp/keeper" + "github.com/sourcenetwork/sourcehub/x/acp/testutil" + "github.com/sourcenetwork/sourcehub/x/acp/types" + hubtestutil "github.com/sourcenetwork/sourcehub/x/hub/testutil" +) + +func setupSimKeeper(t *testing.T) (sdk.Context, *keeper.Keeper) { + acpStoreKey := storetypes.NewKVStoreKey(types.StoreKey) + capStoreKey := storetypes.NewKVStoreKey("capkeeper") + capMemStoreKey := storetypes.NewKVStoreKey("capkeepermem") + + db := dbm.NewMemDB() + stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + stateStore.MountStoreWithDB(acpStoreKey, storetypes.StoreTypeDB, db) + stateStore.MountStoreWithDB(capStoreKey, storetypes.StoreTypeDB, db) + stateStore.MountStoreWithDB(capMemStoreKey, storetypes.StoreTypeDB, db) + require.NoError(t, stateStore.LoadLatestVersion()) + + registry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(registry) + authority := authtypes.NewModuleAddress(govtypes.ModuleName) + capKeeper := capabilitykeeper.NewKeeper(cdc, capStoreKey, capMemStoreKey) + acpCapKeeper := capKeeper.ScopeToModule(types.ModuleName) + + accKeeper := &testutil.AccountKeeperStub{} + k := keeper.NewKeeper( + cdc, + runtime.NewKVStoreService(acpStoreKey), + log.NewNopLogger(), + authority.String(), + accKeeper, + &acpCapKeeper, + hubtestutil.NewHubKeeperStub(), + ) + + ctx := sdk.NewContext(stateStore, cmtproto.Header{}, false, log.NewNopLogger()) + return ctx, &k +} + +func makeSimAccounts() []simtypes.Account { + r := rand.New(rand.NewSource(42)) + return simtypes.RandomAccounts(r, 3) +} + +func TestFindAccount(t *testing.T) { + accs := makeSimAccounts() + addr := accs[0].Address.String() + + found, ok := FindAccount(accs, addr) + require.True(t, ok) + require.Equal(t, accs[0].Address, found.Address) +} + +func TestFindAccountNotFound(t *testing.T) { + accs := makeSimAccounts() + // use a valid bech32 address that isn't in the list + otherAccs := simtypes.RandomAccounts(rand.New(rand.NewSource(99)), 1) + _, ok := FindAccount(accs, otherAccs[0].Address.String()) + require.False(t, ok) +} + +func TestFindAccountPanicsOnInvalidAddress(t *testing.T) { + accs := makeSimAccounts() + require.Panics(t, func() { + FindAccount(accs, "not-bech32") + }) +} + +func TestSimulateMsgCreatePolicy(t *testing.T) { + ctx, k := setupSimKeeper(t) + r := rand.New(rand.NewSource(42)) + accs := makeSimAccounts() + + op := SimulateMsgCreatePolicy(nil, nil, k) + msg, futures, err := op(r, nil, ctx, accs, "test-chain") + require.NoError(t, err) + require.Nil(t, futures) + require.Contains(t, msg.Comment, "not implemented") +} + +func TestSimulateMsgCheckAccess(t *testing.T) { + ctx, k := setupSimKeeper(t) + r := rand.New(rand.NewSource(42)) + accs := makeSimAccounts() + + op := SimulateMsgCheckAccess(nil, nil, k) + msg, futures, err := op(r, nil, ctx, accs, "test-chain") + require.NoError(t, err) + require.Nil(t, futures) + require.Contains(t, msg.Comment, "not implemented") +} + +func TestSimulateMsgPolicyCmd(t *testing.T) { + ctx, k := setupSimKeeper(t) + r := rand.New(rand.NewSource(42)) + accs := makeSimAccounts() + + op := SimulateMsgPolicyCmd(nil, nil, k) + msg, futures, err := op(r, nil, ctx, accs, "test-chain") + require.NoError(t, err) + require.Nil(t, futures) + require.Contains(t, msg.Comment, "not implemented") +} + +func TestSimulateMsgMsgEditPolicy(t *testing.T) { + ctx, k := setupSimKeeper(t) + r := rand.New(rand.NewSource(42)) + accs := makeSimAccounts() + + op := SimulateMsgMsgEditPolicy(nil, nil, k) + msg, futures, err := op(r, nil, ctx, accs, "test-chain") + require.NoError(t, err) + require.Nil(t, futures) + require.Contains(t, msg.Comment, "not implemented") +} diff --git a/x/acp/stores/kv_stores_cosmos_test.go b/x/acp/stores/kv_stores_cosmos_test.go new file mode 100644 index 00000000..3abbd75d --- /dev/null +++ b/x/acp/stores/kv_stores_cosmos_test.go @@ -0,0 +1,100 @@ +package stores + +import ( + "testing" + + "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/stretchr/testify/require" +) + +func setupTestStore(t *testing.T) storetypes.KVStore { + storeKey := storetypes.NewKVStoreKey("test") + db := dbm.NewMemDB() + stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, db) + require.NoError(t, stateStore.LoadLatestVersion()) + return stateStore.GetKVStore(storeKey) +} + +func TestRaccoonKVFromCosmos(t *testing.T) { + cosmosStore := setupTestStore(t) + wrapped := RaccoonKVFromCosmos(cosmosStore) + require.NotNil(t, wrapped) +} + +func TestCosmosKvWrapperSetGet(t *testing.T) { + cosmosStore := setupTestStore(t) + kv := RaccoonKVFromCosmos(cosmosStore) + + err := kv.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + val, err := kv.Get([]byte("key1")) + require.NoError(t, err) + require.Equal(t, []byte("value1"), val) +} + +func TestCosmosKvWrapperHas(t *testing.T) { + cosmosStore := setupTestStore(t) + kv := RaccoonKVFromCosmos(cosmosStore) + + has, err := kv.Has([]byte("missing")) + require.NoError(t, err) + require.False(t, has) + + err = kv.Set([]byte("exists"), []byte("val")) + require.NoError(t, err) + + has, err = kv.Has([]byte("exists")) + require.NoError(t, err) + require.True(t, has) +} + +func TestCosmosKvWrapperDelete(t *testing.T) { + cosmosStore := setupTestStore(t) + kv := RaccoonKVFromCosmos(cosmosStore) + + err := kv.Set([]byte("key"), []byte("val")) + require.NoError(t, err) + + err = kv.Delete([]byte("key")) + require.NoError(t, err) + + has, err := kv.Has([]byte("key")) + require.NoError(t, err) + require.False(t, has) +} + +func TestCosmosKvWrapperGetMissing(t *testing.T) { + cosmosStore := setupTestStore(t) + kv := RaccoonKVFromCosmos(cosmosStore) + + val, err := kv.Get([]byte("nonexistent")) + require.NoError(t, err) + require.Nil(t, val) +} + +func TestCosmosKvWrapperIterator(t *testing.T) { + cosmosStore := setupTestStore(t) + kv := RaccoonKVFromCosmos(cosmosStore) + + err := kv.Set([]byte("a"), []byte("1")) + require.NoError(t, err) + err = kv.Set([]byte("b"), []byte("2")) + require.NoError(t, err) + err = kv.Set([]byte("c"), []byte("3")) + require.NoError(t, err) + + iter := kv.Iterator([]byte("a"), []byte("d")) + defer iter.Close() + + count := 0 + for ; iter.Valid(); iter.Next() { + count++ + } + require.Equal(t, 3, count) +} diff --git a/x/acp/stores/marshaler_test.go b/x/acp/stores/marshaler_test.go new file mode 100644 index 00000000..257c0fba --- /dev/null +++ b/x/acp/stores/marshaler_test.go @@ -0,0 +1,33 @@ +package stores + +import ( + "testing" + + "github.com/sourcenetwork/sourcehub/x/acp/types" + "github.com/stretchr/testify/require" +) + +func TestGogoProtoMarshalerRoundTrip(t *testing.T) { + marshaler := NewGogoProtoMarshaler(func() *types.Params { + return &types.Params{} + }) + + original := types.DefaultParams() + origPtr := &original + bytes, err := marshaler.Marshal(&origPtr) + require.NoError(t, err) + require.NotEmpty(t, bytes) + + decoded, err := marshaler.Unmarshal(bytes) + require.NoError(t, err) + require.Equal(t, original.PolicyCommandMaxExpirationDelta, decoded.PolicyCommandMaxExpirationDelta) +} + +func TestGogoProtoMarshalerUnmarshalInvalid(t *testing.T) { + marshaler := NewGogoProtoMarshaler(func() *types.Params { + return &types.Params{} + }) + + _, err := marshaler.Unmarshal([]byte("invalid-protobuf-data")) + require.Error(t, err) +} diff --git a/x/acp/types/access_decision_test.go b/x/acp/types/access_decision_test.go new file mode 100644 index 00000000..11cb6480 --- /dev/null +++ b/x/acp/types/access_decision_test.go @@ -0,0 +1,91 @@ +package types + +import ( + "testing" + "time" + + prototypes "github.com/cosmos/gogoproto/types" + coretypes "github.com/sourcenetwork/acp_core/pkg/types" + "github.com/stretchr/testify/require" +) + +func makeTestTimestamp(t *testing.T) *Timestamp { + ts, err := prototypes.TimestampProto(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)) + require.NoError(t, err) + return NewTimestamp(ts, 100) +} + +func TestAccessDecisionProduceId(t *testing.T) { + creationTs := makeTestTimestamp(t) + decision := &AccessDecision{ + PolicyId: "policy-1", + Creator: "cosmos1creator", + Actor: "did:example:actor", + CreatorAccSequence: 1, + IssuedHeight: 100, + CreationTime: creationTs, + Operations: []*coretypes.Operation{ + { + Object: coretypes.NewObject("resource", "obj1"), + Permission: "read", + }, + }, + Params: &DecisionParams{ + DecisionExpirationDelta: 10, + ProofExpirationDelta: 5, + TicketExpirationDelta: 20, + }, + } + + id := decision.ProduceId() + require.NotEmpty(t, id) + + // deterministic + id2 := decision.ProduceId() + require.Equal(t, id, id2) +} + +func TestAccessDecisionProduceIdDifferentInputs(t *testing.T) { + creationTs := makeTestTimestamp(t) + params := &DecisionParams{ + DecisionExpirationDelta: 10, + ProofExpirationDelta: 5, + TicketExpirationDelta: 20, + } + + d1 := &AccessDecision{ + PolicyId: "policy-1", + Creator: "cosmos1a", + Actor: "did:example:a", + CreationTime: creationTs, + Operations: []*coretypes.Operation{ + {Object: coretypes.NewObject("res", "1"), Permission: "read"}, + }, + Params: params, + } + d2 := &AccessDecision{ + PolicyId: "policy-2", + Creator: "cosmos1a", + Actor: "did:example:a", + CreationTime: creationTs, + Operations: []*coretypes.Operation{ + {Object: coretypes.NewObject("res", "1"), Permission: "read"}, + }, + Params: params, + } + + require.NotEqual(t, d1.ProduceId(), d2.ProduceId()) +} + +func TestAccessDecisionHashParams(t *testing.T) { + decision := &AccessDecision{ + Params: &DecisionParams{ + DecisionExpirationDelta: 10, + ProofExpirationDelta: 5, + TicketExpirationDelta: 20, + }, + } + hash := decision.hashParams() + require.NotEmpty(t, hash) + require.Len(t, hash, 32) // sha256 +} diff --git a/x/acp/types/codec_test.go b/x/acp/types/codec_test.go new file mode 100644 index 00000000..3132ff78 --- /dev/null +++ b/x/acp/types/codec_test.go @@ -0,0 +1,15 @@ +package types + +import ( + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/stretchr/testify/require" +) + +func TestRegisterInterfaces(t *testing.T) { + registry := codectypes.NewInterfaceRegistry() + require.NotPanics(t, func() { + RegisterInterfaces(registry) + }) +} diff --git a/x/acp/types/commitment_test.go b/x/acp/types/commitment_test.go new file mode 100644 index 00000000..bdc73cd3 --- /dev/null +++ b/x/acp/types/commitment_test.go @@ -0,0 +1,62 @@ +package types + +import ( + "testing" + "time" + + prototypes "github.com/cosmos/gogoproto/types" + "github.com/stretchr/testify/require" +) + +func TestRegistrationsCommitmentIsExpiredAgainst(t *testing.T) { + creationTime := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC) + creationProto, err := prototypes.TimestampProto(creationTime) + require.NoError(t, err) + + commitment := &RegistrationsCommitment{ + Metadata: &RecordMetadata{ + CreationTs: NewTimestamp(creationProto, 100), + }, + Validity: NewDurationFromTimeDuration(10 * time.Minute), + } + + tests := []struct { + name string + nowTime time.Time + block uint64 + expired bool + }{ + {"not expired", creationTime.Add(5 * time.Minute), 105, false}, + {"at boundary", creationTime.Add(10 * time.Minute), 110, false}, + {"expired", creationTime.Add(11 * time.Minute), 111, true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + nowProto, err := prototypes.TimestampProto(tc.nowTime) + require.NoError(t, err) + now := NewTimestamp(nowProto, tc.block) + result, err := commitment.IsExpiredAgainst(now) + require.NoError(t, err) + require.Equal(t, tc.expired, result) + }) + } +} + +func TestRegistrationsCommitmentIsExpiredAgainstBlockCount(t *testing.T) { + commitment := &RegistrationsCommitment{ + Metadata: &RecordMetadata{ + CreationTs: NewTimestamp(nil, 100), + }, + Validity: NewBlockCountDuration(50), + } + + now := NewTimestamp(nil, 160) + expired, err := commitment.IsExpiredAgainst(now) + require.NoError(t, err) + require.True(t, expired) + + now2 := NewTimestamp(nil, 140) + expired2, err := commitment.IsExpiredAgainst(now2) + require.NoError(t, err) + require.False(t, expired2) +} diff --git a/x/acp/types/constants_test.go b/x/acp/types/constants_test.go new file mode 100644 index 00000000..3af90e8d --- /dev/null +++ b/x/acp/types/constants_test.go @@ -0,0 +1,18 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDefaultPolicyCommandMaxExpirationDelta(t *testing.T) { + // 12 hours in seconds + require.Equal(t, uint64(43200), uint64(DefaultPolicyCommandMaxExpirationDelta)) +} + +func TestDefaultRegistrationCommitmentLifetime(t *testing.T) { + require.NotNil(t, DefaultRegistrationCommitmentLifetime) + _, ok := DefaultRegistrationCommitmentLifetime.Duration.(*Duration_ProtoDuration) + require.True(t, ok) +} diff --git a/x/acp/types/errors_test.go b/x/acp/types/errors_test.go new file mode 100644 index 00000000..edb3fea7 --- /dev/null +++ b/x/acp/types/errors_test.go @@ -0,0 +1,45 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewErrInvalidAccAddrErr(t *testing.T) { + cause := fmt.Errorf("bad format") + err := NewErrInvalidAccAddrErr(cause, "cosmos1invalid") + require.Error(t, err) + require.Contains(t, err.Error(), "invalid account address") +} + +func TestNewAccNotFoundErr(t *testing.T) { + err := NewAccNotFoundErr("cosmos1missing") + require.Error(t, err) + require.Contains(t, err.Error(), "account not found") +} + +func TestErrInvalidSigner(t *testing.T) { + require.NotNil(t, ErrInvalidSigner) + require.Contains(t, ErrInvalidSigner.Error(), "expected gov account") +} + +func TestErrorTypeAliases(t *testing.T) { + // verify the type aliases are properly wired + require.NotEqual(t, ErrorType_UNKNOWN, ErrorType_INTERNAL) + require.NotEqual(t, ErrorType_UNAUTHENTICATED, ErrorType_UNAUTHORIZED) + require.NotEqual(t, ErrorType_BAD_INPUT, ErrorType_OPERATION_FORBIDDEN) + require.NotEqual(t, ErrorType_NOT_FOUND, ErrorType_UNKNOWN) +} + +func TestErrorConstructorAliases(t *testing.T) { + err := New("test error", ErrorType_INTERNAL) + require.Error(t, err) + + wrapped := Wrap("wrapped", err) + require.Error(t, wrapped) + + withCause := NewWithCause("msg", fmt.Errorf("cause"), ErrorType_BAD_INPUT) + require.Error(t, withCause) +} diff --git a/x/acp/types/keys_test.go b/x/acp/types/keys_test.go new file mode 100644 index 00000000..7589eecc --- /dev/null +++ b/x/acp/types/keys_test.go @@ -0,0 +1,30 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestModuleName(t *testing.T) { + require.Equal(t, "acp", ModuleName) +} + +func TestStoreKey(t *testing.T) { + require.Equal(t, ModuleName, StoreKey) +} + +func TestMemStoreKey(t *testing.T) { + require.Equal(t, "mem_acp", MemStoreKey) +} + +func TestKeyPrefixes(t *testing.T) { + require.Equal(t, "access_decision/", AccessDecisionRepositoryKeyPrefix) + require.Equal(t, "commitment/", RegistrationsCommitmentKeyPrefix) + require.Equal(t, "amendment_event/", AmendmentEventKeyPrefix) + require.Equal(t, "spc_seen/", SignedPolicyCmdSeenKeyPrefix) +} + +func TestParamsKey(t *testing.T) { + require.Equal(t, []byte("p_acp"), ParamsKey) +} diff --git a/x/acp/types/mapper_test.go b/x/acp/types/mapper_test.go new file mode 100644 index 00000000..1597f40a --- /dev/null +++ b/x/acp/types/mapper_test.go @@ -0,0 +1,88 @@ +package types + +import ( + "testing" + "time" + + "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + dbm "github.com/cosmos/cosmos-db" + sdk "github.com/cosmos/cosmos-sdk/types" + prototypes "github.com/cosmos/gogoproto/types" + coretypes "github.com/sourcenetwork/acp_core/pkg/types" + "github.com/stretchr/testify/require" +) + +func makeTestContext(t *testing.T) sdk.Context { + storeKey := storetypes.NewKVStoreKey("test") + db := dbm.NewMemDB() + stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, db) + require.NoError(t, stateStore.LoadLatestVersion()) + + blockTime := time.Date(2024, time.June, 1, 12, 0, 0, 0, time.UTC) + ctx := sdk.NewContext(stateStore, cmtproto.Header{ + Height: 42, + Time: blockTime, + }, false, log.NewNopLogger()) + return ctx +} + +func TestBuildRecordMetadata(t *testing.T) { + ctx := makeTestContext(t) + md, err := BuildRecordMetadata(ctx, "did:example:actor", "cosmos1creator") + require.NoError(t, err) + require.NotNil(t, md) + require.Equal(t, "did:example:actor", md.OwnerDid) + require.Equal(t, "cosmos1creator", md.TxSigner) + require.NotNil(t, md.CreationTs) + require.Equal(t, uint64(42), md.CreationTs.BlockHeight) +} + +func TestBuildACPSuppliedMetadata(t *testing.T) { + ctx := makeTestContext(t) + sm, err := BuildACPSuppliedMetadata(ctx, "did:example:actor", "cosmos1creator") + require.NoError(t, err) + require.NotNil(t, sm) + require.NotEmpty(t, sm.Blob) +} + +func TestBuildACPSuppliedMetadataWithTime(t *testing.T) { + ctx := makeTestContext(t) + protoTs, err := prototypes.TimestampProto(time.Date(2024, time.March, 1, 0, 0, 0, 0, time.UTC)) + require.NoError(t, err) + ts := NewTimestamp(protoTs, 50) + + sm, err := BuildACPSuppliedMetadataWithTime(ctx, ts, "did:example:actor", "cosmos1creator") + require.NoError(t, err) + require.NotNil(t, sm) + require.NotEmpty(t, sm.Blob) +} + +func TestExtractRecordMetadata(t *testing.T) { + ctx := makeTestContext(t) + original, err := BuildRecordMetadata(ctx, "did:example:actor", "cosmos1creator") + require.NoError(t, err) + + blob, err := original.Marshal() + require.NoError(t, err) + + coreMd := &coretypes.RecordMetadata{ + Supplied: &coretypes.SuppliedMetadata{Blob: blob}, + } + extracted, err := ExtractRecordMetadata(coreMd) + require.NoError(t, err) + require.Equal(t, original.OwnerDid, extracted.OwnerDid) + require.Equal(t, original.TxSigner, extracted.TxSigner) +} + +func TestExtractRecordMetadataInvalidBlob(t *testing.T) { + coreMd := &coretypes.RecordMetadata{ + Supplied: &coretypes.SuppliedMetadata{Blob: []byte("not-valid-protobuf")}, + } + _, err := ExtractRecordMetadata(coreMd) + require.Error(t, err) +} diff --git a/x/acp/types/messages_test.go b/x/acp/types/messages_test.go new file mode 100644 index 00000000..4118640f --- /dev/null +++ b/x/acp/types/messages_test.go @@ -0,0 +1,105 @@ +package types + +import ( + "testing" + + coretypes "github.com/sourcenetwork/acp_core/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestNewMsgBearerPolicyCmd(t *testing.T) { + cmd := NewRegisterObjectCmd(coretypes.NewObject("res", "obj1")) + msg := NewMsgBearerPolicyCmd("cosmos1creator", "bearer-token", "policy-1", cmd) + require.Equal(t, "cosmos1creator", msg.Creator) + require.Equal(t, "bearer-token", msg.BearerToken) + require.Equal(t, "policy-1", msg.PolicyId) + require.Equal(t, cmd, msg.Cmd) +} + +func TestNewMsgCheckAccess(t *testing.T) { + accessReq := &coretypes.AccessRequest{ + Operations: []*coretypes.Operation{ + { + Object: coretypes.NewObject("res", "obj1"), + Permission: "read", + }, + }, + Actor: coretypes.NewActor("did:example:alice"), + } + msg := NewMsgCheckAccess("cosmos1creator", "policy-1", accessReq) + require.Equal(t, "cosmos1creator", msg.Creator) + require.Equal(t, "policy-1", msg.PolicyId) + require.Equal(t, accessReq, msg.AccessRequest) +} + +func TestNewMsgCreatePolicy(t *testing.T) { + msg := NewMsgCreatePolicy("cosmos1creator", "policy-yaml", coretypes.PolicyMarshalingType_YAML) + require.Equal(t, "cosmos1creator", msg.Creator) + require.Equal(t, "policy-yaml", msg.Policy) + require.Equal(t, coretypes.PolicyMarshalingType_YAML, msg.MarshalType) +} + +func TestNewMsgDirectPolicyCmd(t *testing.T) { + cmd := NewRegisterObjectCmd(coretypes.NewObject("res", "obj1")) + msg := NewMsgDirectPolicyCmd("cosmos1creator", "policy-1", cmd) + require.Equal(t, "cosmos1creator", msg.Creator) + require.Equal(t, "policy-1", msg.PolicyId) + require.Equal(t, cmd, msg.Cmd) +} + +func TestNewMsgEditPolicy(t *testing.T) { + msg := NewMsgEditPolicy("cosmos1creator", "policy-1", "new-policy-yaml", coretypes.PolicyMarshalingType_YAML) + require.Equal(t, "cosmos1creator", msg.Creator) + require.Equal(t, "policy-1", msg.PolicyId) + require.Equal(t, "new-policy-yaml", msg.Policy) + require.Equal(t, coretypes.PolicyMarshalingType_YAML, msg.MarshalType) +} + +func TestNewMsgSignedPolicyCmd(t *testing.T) { + msg := NewMsgSignedPolicyCmd("cosmos1creator", "payload", MsgSignedPolicyCmd_JWS) + require.Equal(t, "cosmos1creator", msg.Creator) + require.Equal(t, "payload", msg.Payload) + require.Equal(t, MsgSignedPolicyCmd_JWS, msg.Type) +} + +func TestNewMsgSignedPolicyCmdFromJWS(t *testing.T) { + msg := NewMsgSignedPolicyCmdFromJWS("cosmos1creator", "jws-payload") + require.Equal(t, "cosmos1creator", msg.Creator) + require.Equal(t, "jws-payload", msg.Payload) + require.Equal(t, MsgSignedPolicyCmd_JWS, msg.Type) +} + +func TestMsgUpdateParamsValidateBasic(t *testing.T) { + tests := []struct { + name string + msg MsgUpdateParams + wantErr bool + }{ + { + "valid authority", + MsgUpdateParams{ + Authority: "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + Params: DefaultParams(), + }, + false, + }, + { + "invalid authority", + MsgUpdateParams{ + Authority: "invalid", + Params: DefaultParams(), + }, + true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/acp/types/params_test.go b/x/acp/types/params_test.go new file mode 100644 index 00000000..8c6c744d --- /dev/null +++ b/x/acp/types/params_test.go @@ -0,0 +1,41 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestNewParams(t *testing.T) { + validity := NewDurationFromTimeDuration(5 * time.Minute) + p := NewParams(1000, validity) + require.Equal(t, uint64(1000), p.PolicyCommandMaxExpirationDelta) + require.Equal(t, validity, p.RegistrationsCommitmentValidity) +} + +func TestDefaultParams(t *testing.T) { + p := DefaultParams() + require.Equal(t, uint64(DefaultPolicyCommandMaxExpirationDelta), p.PolicyCommandMaxExpirationDelta) + require.NotNil(t, p.RegistrationsCommitmentValidity) +} + +func TestParamsValidate(t *testing.T) { + p := DefaultParams() + require.NoError(t, p.Validate()) + + // zero values also pass since Validate is a no-op + zero := Params{} + require.NoError(t, zero.Validate()) +} + +func TestParamKeyTable(t *testing.T) { + kt := ParamKeyTable() + require.NotNil(t, kt) +} + +func TestParamSetPairs(t *testing.T) { + p := DefaultParams() + pairs := p.ParamSetPairs() + require.Empty(t, pairs) +} diff --git a/x/acp/types/policy_cmd_test.go b/x/acp/types/policy_cmd_test.go new file mode 100644 index 00000000..d73c5672 --- /dev/null +++ b/x/acp/types/policy_cmd_test.go @@ -0,0 +1,82 @@ +package types + +import ( + "testing" + + coretypes "github.com/sourcenetwork/acp_core/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestNewSetRelationshipCmd(t *testing.T) { + rel := coretypes.NewActorRelationship("resource", "obj1", "reader", "did:example:alice") + cmd := NewSetRelationshipCmd(rel) + require.NotNil(t, cmd) + setCmd, ok := cmd.Cmd.(*PolicyCmd_SetRelationshipCmd) + require.True(t, ok) + require.Equal(t, rel, setCmd.SetRelationshipCmd.Relationship) +} + +func TestNewDeleteRelationshipCmd(t *testing.T) { + rel := coretypes.NewActorRelationship("resource", "obj1", "reader", "did:example:alice") + cmd := NewDeleteRelationshipCmd(rel) + require.NotNil(t, cmd) + delCmd, ok := cmd.Cmd.(*PolicyCmd_DeleteRelationshipCmd) + require.True(t, ok) + require.Equal(t, rel, delCmd.DeleteRelationshipCmd.Relationship) +} + +func TestNewRegisterObjectCmd(t *testing.T) { + obj := coretypes.NewObject("resource", "obj1") + cmd := NewRegisterObjectCmd(obj) + require.NotNil(t, cmd) + regCmd, ok := cmd.Cmd.(*PolicyCmd_RegisterObjectCmd) + require.True(t, ok) + require.Equal(t, obj, regCmd.RegisterObjectCmd.Object) +} + +func TestNewArchiveObjectCmd(t *testing.T) { + obj := coretypes.NewObject("resource", "obj1") + cmd := NewArchiveObjectCmd(obj) + require.NotNil(t, cmd) + archCmd, ok := cmd.Cmd.(*PolicyCmd_ArchiveObjectCmd) + require.True(t, ok) + require.Equal(t, obj, archCmd.ArchiveObjectCmd.Object) +} + +func TestNewCommitRegistrationCmd(t *testing.T) { + commitment := []byte("commitment-hash") + cmd := NewCommitRegistrationCmd(commitment) + require.NotNil(t, cmd) + commitCmd, ok := cmd.Cmd.(*PolicyCmd_CommitRegistrationsCmd) + require.True(t, ok) + require.Equal(t, commitment, commitCmd.CommitRegistrationsCmd.Commitment) +} + +func TestNewRevealRegistrationCmd(t *testing.T) { + proof := &RegistrationProof{ + Object: coretypes.NewObject("resource", "obj1"), + } + cmd := NewRevealRegistrationCmd(42, proof) + require.NotNil(t, cmd) + revealCmd, ok := cmd.Cmd.(*PolicyCmd_RevealRegistrationCmd) + require.True(t, ok) + require.Equal(t, proof, revealCmd.RevealRegistrationCmd.Proof) + require.Equal(t, uint64(42), revealCmd.RevealRegistrationCmd.RegistrationsCommitmentId) +} + +func TestNewFlagHijackAttemptCmd(t *testing.T) { + cmd := NewFlagHijackAttemptCmd(99) + require.NotNil(t, cmd) + flagCmd, ok := cmd.Cmd.(*PolicyCmd_FlagHijackAttemptCmd) + require.True(t, ok) + require.Equal(t, uint64(99), flagCmd.FlagHijackAttemptCmd.EventId) +} + +func TestNewUnarchiveObjectCmd(t *testing.T) { + obj := coretypes.NewObject("resource", "obj1") + cmd := NewUnarchiveObjectCmd(obj) + require.NotNil(t, cmd) + unarchCmd, ok := cmd.Cmd.(*PolicyCmd_UnarchiveObjectCmd) + require.True(t, ok) + require.Equal(t, obj, unarchCmd.UnarchiveObjectCmd.Object) +} diff --git a/x/acp/types/time_test.go b/x/acp/types/time_test.go new file mode 100644 index 00000000..4f11b987 --- /dev/null +++ b/x/acp/types/time_test.go @@ -0,0 +1,108 @@ +package types + +import ( + "testing" + "time" + + prototypes "github.com/cosmos/gogoproto/types" + "github.com/stretchr/testify/require" +) + +func TestNewBlockCountDuration(t *testing.T) { + d := NewBlockCountDuration(100) + require.NotNil(t, d) + bc, ok := d.Duration.(*Duration_BlockCount) + require.True(t, ok) + require.Equal(t, uint64(100), bc.BlockCount) +} + +func TestNewDurationFromTimeDuration(t *testing.T) { + d := NewDurationFromTimeDuration(5 * time.Minute) + require.NotNil(t, d) + pd, ok := d.Duration.(*Duration_ProtoDuration) + require.True(t, ok) + require.NotNil(t, pd.ProtoDuration) +} + +func TestTimestampToISOString(t *testing.T) { + goTime := time.Date(2024, time.March, 15, 10, 30, 0, 0, time.UTC) + protoTs, err := prototypes.TimestampProto(goTime) + require.NoError(t, err) + + ts := NewTimestamp(protoTs, 42) + iso, err := ts.ToISOString() + require.NoError(t, err) + require.Equal(t, "2024-03-15T10:30:00Z", iso) +} + +func TestTimestampIsAfterBlockCount(t *testing.T) { + ts := NewTimestamp(nil, 100) + duration := NewBlockCountDuration(50) + + tests := []struct { + name string + nowBlock uint64 + expected bool + }{ + {"before expiry", 140, false}, + {"at expiry", 150, false}, + {"after expiry", 151, true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + now := NewTimestamp(nil, tc.nowBlock) + result, err := ts.IsAfter(duration, now) + require.NoError(t, err) + require.Equal(t, tc.expected, result) + }) + } +} + +func TestTimestampIsAfterProtoDuration(t *testing.T) { + goTime := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC) + protoTs, err := prototypes.TimestampProto(goTime) + require.NoError(t, err) + + ts := NewTimestamp(protoTs, 0) + duration := NewDurationFromTimeDuration(1 * time.Hour) + + tests := []struct { + name string + nowTime time.Time + expected bool + }{ + {"before expiry", time.Date(2024, time.January, 1, 0, 30, 0, 0, time.UTC), false}, + {"at expiry", time.Date(2024, time.January, 1, 1, 0, 0, 0, time.UTC), false}, + {"after expiry", time.Date(2024, time.January, 1, 1, 0, 1, 0, time.UTC), true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + nowProto, err := prototypes.TimestampProto(tc.nowTime) + require.NoError(t, err) + now := NewTimestamp(nowProto, 0) + result, err := ts.IsAfter(duration, now) + require.NoError(t, err) + require.Equal(t, tc.expected, result) + }) + } +} + +func TestTimestampIsAfterNilDurationPanics(t *testing.T) { + ts := NewTimestamp(nil, 100) + now := NewTimestamp(nil, 200) + duration := &Duration{Duration: nil} + + require.Panics(t, func() { + ts.IsAfter(duration, now) + }) +} + +func TestNewTimestamp(t *testing.T) { + goTime := time.Date(2024, time.June, 1, 0, 0, 0, 0, time.UTC) + protoTs, err := prototypes.TimestampProto(goTime) + require.NoError(t, err) + + ts := NewTimestamp(protoTs, 42) + require.Equal(t, uint64(42), ts.BlockHeight) + require.Equal(t, protoTs, ts.ProtoTs) +} diff --git a/x/acp/utils/utils_test.go b/x/acp/utils/utils_test.go new file mode 100644 index 00000000..37f7a6f8 --- /dev/null +++ b/x/acp/utils/utils_test.go @@ -0,0 +1,81 @@ +package utils + +import ( + "crypto/sha256" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHashTx(t *testing.T) { + tests := []struct { + name string + txBytes []byte + }{ + {"empty input", []byte{}}, + {"single byte", []byte{0x42}}, + {"typical tx bytes", []byte("some-tx-payload-bytes")}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := HashTx(tc.txBytes) + require.Len(t, result, sha256.Size) + + expected := sha256.Sum256(tc.txBytes) + require.Equal(t, expected[:], result) + }) + } +} + +func TestHashTxDeterministic(t *testing.T) { + input := []byte("deterministic-test") + a := HashTx(input) + b := HashTx(input) + require.Equal(t, a, b) +} + +func TestHashTxDifferentInputs(t *testing.T) { + a := HashTx([]byte("input-a")) + b := HashTx([]byte("input-b")) + require.NotEqual(t, a, b) +} + +func TestWriteBytes(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + {"nil slice", nil}, + {"empty slice", []byte{}}, + {"non-empty data", []byte("hello")}, + {"single byte", []byte{0xFF}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + h := sha256.New() + WriteBytes(h, tc.data) + + // manually build expected hash state + expected := sha256.New() + var lenBuf [8]byte + binary.BigEndian.PutUint64(lenBuf[:], uint64(len(tc.data))) + expected.Write(lenBuf[:]) + if len(tc.data) > 0 { + expected.Write(tc.data) + } + require.Equal(t, expected.Sum(nil), h.Sum(nil)) + }) + } +} + +func TestWriteBytesLengthPrefix(t *testing.T) { + // different-length inputs with same content prefix should produce different hashes + h1 := sha256.New() + WriteBytes(h1, []byte("ab")) + + h2 := sha256.New() + WriteBytes(h2, []byte("abc")) + + require.NotEqual(t, h1.Sum(nil), h2.Sum(nil)) +} From ce2bc93aaa3e26ac3314deb302ec93c9253747b9 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 22 Mar 2026 16:27:12 +0700 Subject: [PATCH 3/6] Fixes --- utils/span.go | 6 +++- x/acp/capability/manager.go | 12 +++---- x/acp/capability/manager_test.go | 15 +++------ x/acp/did/types.go | 3 ++ x/acp/did/types_test.go | 8 ++--- x/hub/keeper/jws_token.go | 44 +++++++++++++++++-------- x/hub/keeper/msg_invalidate_jws.go | 5 ++- x/hub/keeper/msg_invalidate_jws_test.go | 6 ++-- 8 files changed, 60 insertions(+), 39 deletions(-) diff --git a/utils/span.go b/utils/span.go index 6fb76a72..06028f9f 100644 --- a/utils/span.go +++ b/utils/span.go @@ -75,6 +75,10 @@ func GetMsgSpan(ctx sdk.Context) *MsgSpan { // FinalizeSpan ends the span duration frame, transforms it into an SDK Event and emits it using the event manager func FinalizeSpan(ctx sdk.Context) { - event := GetMsgSpan(ctx).ToEvent() + span := GetMsgSpan(ctx) + if span == nil { + return + } + event := span.ToEvent() ctx.EventManager().EmitEvent(event) } diff --git a/x/acp/capability/manager.go b/x/acp/capability/manager.go index 0d675452..a9e12f18 100644 --- a/x/acp/capability/manager.go +++ b/x/acp/capability/manager.go @@ -130,12 +130,10 @@ func (m *PolicyCapabilityManager) isOwnedByAcpModule(ctx sdk.Context, capability return false, fmt.Errorf("looking up capability owner: %v", err) } - mods = utils.FilterSlice(mods, func(name string) bool { - return name != types.ModuleName - }) - - if len(mods) == 0 { - return false, nil + for _, mod := range mods { + if mod == types.ModuleName { + return true, nil + } } - return true, nil + return false, nil } diff --git a/x/acp/capability/manager_test.go b/x/acp/capability/manager_test.go index bc873dc9..7f33e447 100644 --- a/x/acp/capability/manager_test.go +++ b/x/acp/capability/manager_test.go @@ -99,10 +99,8 @@ func TestValidateCapability(t *testing.T) { err = otherMgr.Claim(ctx, cap) require.NoError(t, err) - // validate from the other module's perspective + // validate from the other module's perspective — acp is an owner, so it passes err = otherMgr.Validate(ctx, cap) - // Filter removes "acp", leaving ["other"]. len > 0 => returns true. - // So Validate passes, but for the wrong reason. require.NoError(t, err) } @@ -134,19 +132,14 @@ func TestGetOwnerModuleNoClaimer(t *testing.T) { require.Error(t, err) } -func TestIsOwnedByAcpModuleInvertedLogic(t *testing.T) { - // When only acp owns the capability, the filter removes "acp", - // leaving an empty list, so isOwnedByAcpModule returns false. - // This means Validate would fail for a legitimately issued capability - // if no other module has claimed it yet. +func TestValidateAcpOnlyOwner(t *testing.T) { ctx, acpScoped, _ := setupCapKeeper(t) acpMgr := NewPolicyCapabilityManager(acpScoped) cap, err := acpMgr.Issue(ctx, "policy-1") require.NoError(t, err) - // Validate should pass since acp issued it, but due to the inverted logic, - // it returns ErrInvalidCapability + // Validate should pass since acp issued it err = acpMgr.Validate(ctx, cap) - require.Error(t, err, "isOwnedByAcpModule incorrectly returns false when only acp owns capability") + require.NoError(t, err) } diff --git a/x/acp/did/types.go b/x/acp/did/types.go index fe0ee576..d99592fc 100644 --- a/x/acp/did/types.go +++ b/x/acp/did/types.go @@ -42,6 +42,9 @@ func IssueDID(acc sdk.AccountI) (string, error) { // DIDFromPubKey constructs and returns a DID from a public key. func DIDFromPubKey(pk cryptotypes.PubKey) (string, error) { + if pk == nil { + return "", fmt.Errorf("account public key is nil") + } var keyType crypto.KeyType switch t := pk.(type) { case *secp256k1.PubKey: diff --git a/x/acp/did/types_test.go b/x/acp/did/types_test.go index 37e334bd..5099fcea 100644 --- a/x/acp/did/types_test.go +++ b/x/acp/did/types_test.go @@ -40,10 +40,10 @@ func TestIssueDID(t *testing.T) { require.Contains(t, did, "did:key:") } -func TestDIDFromPubKeyNilPanics(t *testing.T) { - require.Panics(t, func() { - DIDFromPubKey(nil) - }) +func TestDIDFromPubKeyNilReturnsError(t *testing.T) { + _, err := DIDFromPubKey(nil) + require.Error(t, err) + require.Contains(t, err.Error(), "nil") } func TestIssueModuleDID(t *testing.T) { diff --git a/x/hub/keeper/jws_token.go b/x/hub/keeper/jws_token.go index 477ace8a..a1365e35 100644 --- a/x/hub/keeper/jws_token.go +++ b/x/hub/keeper/jws_token.go @@ -83,27 +83,30 @@ func (k *Keeper) SetJWSToken(ctx context.Context, record *types.JWSTokenRecord) } // GetJWSToken retrieves a JWS token record by its hash. -// Returns the record and true if found, or nil and false if not found or unmarshal fails. -func (k *Keeper) GetJWSToken(ctx context.Context, tokenHash string) (*types.JWSTokenRecord, bool) { +// Returns the record and true if found, or (nil, false, nil) if not found. +// Returns a non-nil error if the record exists but cannot be decoded. +func (k *Keeper) GetJWSToken(ctx context.Context, tokenHash string) (*types.JWSTokenRecord, bool, error) { store := k.jwsTokenStore(ctx) bz := store.Get([]byte(tokenHash)) if bz == nil { - return nil, false + return nil, false, nil } var record types.JWSTokenRecord if err := k.cdc.Unmarshal(bz, &record); err != nil { - k.Logger().Error("failed to unmarshal JWS token record", "hash", tokenHash, "error", err) - return nil, false + return nil, false, fmt.Errorf("failed to unmarshal JWS token record %s: %w", tokenHash, err) } - return &record, true + return &record, true, nil } // DeleteJWSToken removes a JWS token record and its indices. func (k *Keeper) DeleteJWSToken(ctx context.Context, tokenHash string) error { // First, get the record to access DID and account for index cleanup - record, found := k.GetJWSToken(ctx, tokenHash) + record, found, err := k.GetJWSToken(ctx, tokenHash) + if err != nil { + return errorsmod.Wrap(err, "decoding JWS token") + } if !found { return errorsmod.Wrap(types.ErrJWSTokenNotFound, tokenHash) } @@ -167,7 +170,10 @@ func (k *Keeper) GetJWSTokensByDID(ctx context.Context, did string) ([]*types.JW continue } - record, found := k.GetJWSToken(ctx, tokenHash) + record, found, err := k.GetJWSToken(ctx, tokenHash) + if err != nil { + return nil, err + } if found { records = append(records, record) } @@ -194,7 +200,10 @@ func (k *Keeper) GetJWSTokensByAccount(ctx context.Context, account string) ([]* continue } - record, found := k.GetJWSToken(ctx, tokenHash) + record, found, err := k.GetJWSToken(ctx, tokenHash) + if err != nil { + return nil, err + } if found { records = append(records, record) } @@ -221,7 +230,10 @@ func (k *Keeper) UpdateJWSTokenStatus(ctx context.Context, tokenHash string, sta return errorsmod.Wrap(types.ErrInvalidInput, "token hash cannot be empty") } - record, found := k.GetJWSToken(ctx, tokenHash) + record, found, err := k.GetJWSToken(ctx, tokenHash) + if err != nil { + return errorsmod.Wrap(err, "decoding JWS token") + } if !found { return errorsmod.Wrap(types.ErrJWSTokenNotFound, tokenHash) } @@ -245,7 +257,10 @@ func (k *Keeper) RecordJWSTokenUsage(ctx context.Context, tokenHash string) erro return errorsmod.Wrap(types.ErrInvalidInput, "token hash cannot be empty") } - record, found := k.GetJWSToken(ctx, tokenHash) + record, found, err := k.GetJWSToken(ctx, tokenHash) + if err != nil { + return errorsmod.Wrap(err, "decoding JWS token") + } if !found { return errorsmod.Wrap(types.ErrJWSTokenNotFound, tokenHash) } @@ -295,14 +310,17 @@ func (k *Keeper) StoreOrUpdateJWSToken( tokenHash := types.HashJWSToken(bearerToken) // Check if token already exists - _, found := k.GetJWSToken(ctx, tokenHash) + _, found, err := k.GetJWSToken(ctx, tokenHash) + if err != nil { + return errorsmod.Wrap(err, "decoding JWS token") + } if found { // Token exists, update usage timestamp return k.RecordJWSTokenUsage(ctx, tokenHash) } // Validate that new tokens aren't already expired - if !expiresAt.IsZero() && expiresAt.Before(sdk.UnwrapSDKContext(ctx).BlockTime()) { + if !expiresAt.IsZero() && !expiresAt.After(sdk.UnwrapSDKContext(ctx).BlockTime()) { return errorsmod.Wrap(types.ErrJWSTokenExpired, "cannot create token with expiration time in the past") } diff --git a/x/hub/keeper/msg_invalidate_jws.go b/x/hub/keeper/msg_invalidate_jws.go index 13028a9c..6e5c6a95 100644 --- a/x/hub/keeper/msg_invalidate_jws.go +++ b/x/hub/keeper/msg_invalidate_jws.go @@ -18,7 +18,10 @@ func (k *Keeper) InvalidateJWS(goCtx context.Context, req *types.MsgInvalidateJW ctx := sdk.UnwrapSDKContext(goCtx) // Get the JWS token record - record, found := k.GetJWSToken(goCtx, req.TokenHash) + record, found, err := k.GetJWSToken(goCtx, req.TokenHash) + if err != nil { + return nil, errorsmod.Wrap(err, "decoding JWS token") + } if !found { return nil, errorsmod.Wrapf(types.ErrJWSTokenNotFound, "token hash: %s", req.TokenHash) } diff --git a/x/hub/keeper/msg_invalidate_jws_test.go b/x/hub/keeper/msg_invalidate_jws_test.go index 50d71882..ffdec137 100644 --- a/x/hub/keeper/msg_invalidate_jws_test.go +++ b/x/hub/keeper/msg_invalidate_jws_test.go @@ -93,7 +93,8 @@ func TestMsgInvalidateJWS(t *testing.T) { expErr: false, verifyFunc: func(t *testing.T, resp *types.MsgInvalidateJWSResponse) { require.True(t, resp.Success) - record, found := k.GetJWSToken(sdkCtx, tokenHash1) + record, found, err := k.GetJWSToken(sdkCtx, tokenHash1) + require.NoError(t, err) require.True(t, found) require.Equal(t, types.JWSTokenStatus_STATUS_INVALID, record.Status) require.Equal(t, authorizedAccount, record.InvalidatedBy) @@ -112,7 +113,8 @@ func TestMsgInvalidateJWS(t *testing.T) { expErr: false, verifyFunc: func(t *testing.T, resp *types.MsgInvalidateJWSResponse) { require.True(t, resp.Success) - record, found := k.GetJWSToken(sdkCtx, tokenHash2) + record, found, err := k.GetJWSToken(sdkCtx, tokenHash2) + require.NoError(t, err) require.True(t, found) require.Equal(t, types.JWSTokenStatus_STATUS_INVALID, record.Status) }, From 9dce00ac86716257aa66d864abfe3f89dcc1f526 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 22 Mar 2026 16:35:31 +0700 Subject: [PATCH 4/6] Fix tests, cleanup --- x/acp/capability/manager_test.go | 4 ++-- x/acp/did/did_test.go | 1 + x/acp/keeper/module_acp_test.go | 25 ++++++++++++++++++------- x/acp/stores/marshaler_test.go | 1 + x/acp/types/time_test.go | 2 +- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/x/acp/capability/manager_test.go b/x/acp/capability/manager_test.go index 7f33e447..7d59ece0 100644 --- a/x/acp/capability/manager_test.go +++ b/x/acp/capability/manager_test.go @@ -18,12 +18,12 @@ import ( func setupCapKeeper(t *testing.T) (sdk.Context, *capabilitykeeper.ScopedKeeper, *capabilitykeeper.ScopedKeeper) { capStoreKey := storetypes.NewKVStoreKey("capkeeper") - capMemStoreKey := storetypes.NewKVStoreKey("capkeepermem") + capMemStoreKey := storetypes.NewMemoryStoreKey("capkeepermem") db := dbm.NewMemDB() stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) stateStore.MountStoreWithDB(capStoreKey, storetypes.StoreTypeDB, db) - stateStore.MountStoreWithDB(capMemStoreKey, storetypes.StoreTypeDB, db) + stateStore.MountStoreWithDB(capMemStoreKey, storetypes.StoreTypeMemory, nil) require.NoError(t, stateStore.LoadLatestVersion()) registry := codectypes.NewInterfaceRegistry() diff --git a/x/acp/did/did_test.go b/x/acp/did/did_test.go index 0cdd3b82..3e2cadd1 100644 --- a/x/acp/did/did_test.go +++ b/x/acp/did/did_test.go @@ -12,6 +12,7 @@ func TestProduceDID(t *testing.T) { require.NotEmpty(t, did) require.NotNil(t, signer) require.Contains(t, did, "did:key:") + require.NoError(t, IsValidDID(did)) } func TestProduceDIDUniqueness(t *testing.T) { diff --git a/x/acp/keeper/module_acp_test.go b/x/acp/keeper/module_acp_test.go index b4b46c25..c671e514 100644 --- a/x/acp/keeper/module_acp_test.go +++ b/x/acp/keeper/module_acp_test.go @@ -128,11 +128,22 @@ resources: } func Test_ModulePolicyCmdForActorAccount_ModuleCannotUsePolicyWithoutClaimingCapability(t *testing.T) { - ctx, k, accKeep, _ := setupKeeperWithCapability(t) + ctx, k, accKeep, capK := setupKeeperWithCapability(t) - // Given Policy created by module without a claimed capability - pol := "name: test" - _, cap, err := k.CreateModulePolicy(ctx, pol, coretypes.PolicyMarshalingType_YAML, "external") + // Given Policy created by module with a claimed capability + pol := ` +name: test +resources: +- name: file +` + moduleName := "external" + _, cap, err := k.CreateModulePolicy(ctx, pol, coretypes.PolicyMarshalingType_YAML, moduleName) + require.NoError(t, err) + + // And capability is claimed by external + scopedKeeper := capK.ScopeToModule(moduleName) + manager := capability.NewPolicyCapabilityManager(&scopedKeeper) + err = manager.Claim(ctx, cap) require.NoError(t, err) // When module issues a policy cmd to an actor acc @@ -141,7 +152,7 @@ func Test_ModulePolicyCmdForActorAccount_ModuleCannotUsePolicyWithoutClaimingCap accAddr := accKeep.FirstAcc().GetAddress().String() result, err := k.ModulePolicyCmdForActorAccount(ctx, cap, cmd, accAddr, signer) - // Then cmd is reject due to invalid capability - require.Nil(t, result) - require.ErrorIs(t, err, capability.ErrInvalidCapability) + // Then cmd is accepted + require.NoError(t, err) + require.NotNil(t, result) } diff --git a/x/acp/stores/marshaler_test.go b/x/acp/stores/marshaler_test.go index 257c0fba..098bd8c4 100644 --- a/x/acp/stores/marshaler_test.go +++ b/x/acp/stores/marshaler_test.go @@ -21,6 +21,7 @@ func TestGogoProtoMarshalerRoundTrip(t *testing.T) { decoded, err := marshaler.Unmarshal(bytes) require.NoError(t, err) require.Equal(t, original.PolicyCommandMaxExpirationDelta, decoded.PolicyCommandMaxExpirationDelta) + require.Equal(t, original.RegistrationsCommitmentValidity, decoded.RegistrationsCommitmentValidity) } func TestGogoProtoMarshalerUnmarshalInvalid(t *testing.T) { diff --git a/x/acp/types/time_test.go b/x/acp/types/time_test.go index 4f11b987..e8729527 100644 --- a/x/acp/types/time_test.go +++ b/x/acp/types/time_test.go @@ -92,7 +92,7 @@ func TestTimestampIsAfterNilDurationPanics(t *testing.T) { now := NewTimestamp(nil, 200) duration := &Duration{Duration: nil} - require.Panics(t, func() { + require.PanicsWithValue(t, "invalid duration", func() { ts.IsAfter(duration, now) }) } From 74c800b47fd83314f7dced245a3eb6a516863d6c Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 22 Mar 2026 16:37:31 +0700 Subject: [PATCH 5/6] Verify capability was claimed by a module in dispatchModulePolicyCmd --- x/acp/keeper/module_acp.go | 9 ++++++++- x/acp/keeper/module_acp_test.go | 25 +++++++------------------ 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/x/acp/keeper/module_acp.go b/x/acp/keeper/module_acp.go index 4b8182f4..e77622a1 100644 --- a/x/acp/keeper/module_acp.go +++ b/x/acp/keeper/module_acp.go @@ -130,7 +130,14 @@ func (k *Keeper) ModulePolicyCmdForActorDID(goCtx context.Context, capability *c func (k *Keeper) dispatchModulePolicyCmd(goCtx context.Context, capability *capability.PolicyCapability, cmd *types.PolicyCmd, actorDID string, txSigner string) (*types.PolicyCmdResult, error) { ctx := sdk.UnwrapSDKContext(goCtx) - err := k.getPolicyCapabilityManager(ctx).Validate(ctx, capability) + capManager := k.getPolicyCapabilityManager(ctx) + err := capManager.Validate(ctx, capability) + if err != nil { + return nil, err + } + + // Verify the capability was claimed by a module + _, err = capManager.GetOwnerModule(ctx, capability) if err != nil { return nil, err } diff --git a/x/acp/keeper/module_acp_test.go b/x/acp/keeper/module_acp_test.go index c671e514..a64f5679 100644 --- a/x/acp/keeper/module_acp_test.go +++ b/x/acp/keeper/module_acp_test.go @@ -128,22 +128,11 @@ resources: } func Test_ModulePolicyCmdForActorAccount_ModuleCannotUsePolicyWithoutClaimingCapability(t *testing.T) { - ctx, k, accKeep, capK := setupKeeperWithCapability(t) - - // Given Policy created by module with a claimed capability - pol := ` -name: test -resources: -- name: file -` - moduleName := "external" - _, cap, err := k.CreateModulePolicy(ctx, pol, coretypes.PolicyMarshalingType_YAML, moduleName) - require.NoError(t, err) + ctx, k, accKeep, _ := setupKeeperWithCapability(t) - // And capability is claimed by external - scopedKeeper := capK.ScopeToModule(moduleName) - manager := capability.NewPolicyCapabilityManager(&scopedKeeper) - err = manager.Claim(ctx, cap) + // Given Policy created by module without a claimed capability + pol := "name: test" + _, cap, err := k.CreateModulePolicy(ctx, pol, coretypes.PolicyMarshalingType_YAML, "external") require.NoError(t, err) // When module issues a policy cmd to an actor acc @@ -152,7 +141,7 @@ resources: accAddr := accKeep.FirstAcc().GetAddress().String() result, err := k.ModulePolicyCmdForActorAccount(ctx, cap, cmd, accAddr, signer) - // Then cmd is accepted - require.NoError(t, err) - require.NotNil(t, result) + // Then cmd is rejected because capability was not claimed + require.Nil(t, result) + require.ErrorIs(t, err, capability.ErrInvalidCapability) } From 8bbe7e77aaaf8f13c8f69c05a41332109bfaa460 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 22 Mar 2026 16:54:54 +0700 Subject: [PATCH 6/6] Jws token fix --- x/hub/keeper/jws_token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/hub/keeper/jws_token.go b/x/hub/keeper/jws_token.go index a1365e35..c02d395f 100644 --- a/x/hub/keeper/jws_token.go +++ b/x/hub/keeper/jws_token.go @@ -286,7 +286,7 @@ func (k *Keeper) CheckAndUpdateExpiredTokens(ctx context.Context) error { } // Check if token is expired - if record.ExpiresAt.Before(currentTime) { + if !record.ExpiresAt.After(currentTime) { // Mark as invalid if err := k.UpdateJWSTokenStatus(ctx, record.TokenHash, types.JWSTokenStatus_STATUS_INVALID, ""); err != nil { k.Logger().Error("failed to update expired token status", "hash", record.TokenHash, "error", err)