From e8a0baaea1395e1ced7be258459db958151f2a6b Mon Sep 17 00:00:00 2001 From: Effi-S Date: Tue, 18 Aug 2026 13:11:39 +0300 Subject: [PATCH] Added token unit tests + Removed stale Mock + stale Transaction Interface" Signed-off-by: Effi-S --- token/services/tokens/generate.go | 1 - token/services/tokens/mock/transaction.go | 353 ------------------ token/services/tokens/tokens.go | 20 - token/services/tokens/tokens_coverage_test.go | 198 ++++++++++ token/services/tokens/tokens_internal_test.go | 110 ++++++ token/services/tokens/tokens_test.go | 123 ++++++ 6 files changed, 431 insertions(+), 374 deletions(-) delete mode 100644 token/services/tokens/mock/transaction.go create mode 100644 token/services/tokens/tokens_coverage_test.go create mode 100644 token/services/tokens/tokens_internal_test.go diff --git a/token/services/tokens/generate.go b/token/services/tokens/generate.go index 7314204461..90b0a5de31 100644 --- a/token/services/tokens/generate.go +++ b/token/services/tokens/generate.go @@ -10,7 +10,6 @@ package tokens //go:generate counterfeiter -o mock/tms_provider.go . TMSProvider //go:generate counterfeiter -o mock/network_provider.go . NetworkProvider //go:generate counterfeiter -o mock/metadata.go . MetaData -//go:generate counterfeiter -o mock/transaction.go . Transaction //go:generate counterfeiter -o mock/cache.go . Cache // External and shared interfaces diff --git a/token/services/tokens/mock/transaction.go b/token/services/tokens/mock/transaction.go deleted file mode 100644 index 4c46bd4347..0000000000 --- a/token/services/tokens/mock/transaction.go +++ /dev/null @@ -1,353 +0,0 @@ -// Code generated by counterfeiter. DO NOT EDIT. -package mock - -import ( - "sync" - - "github.com/LFDT-Panurus/panurus/token" - "github.com/LFDT-Panurus/panurus/token/services/tokens" -) - -type FakeTransaction struct { - ChannelStub func() string - channelMutex sync.RWMutex - channelArgsForCall []struct { - } - channelReturns struct { - result1 string - } - channelReturnsOnCall map[int]struct { - result1 string - } - IDStub func() string - iDMutex sync.RWMutex - iDArgsForCall []struct { - } - iDReturns struct { - result1 string - } - iDReturnsOnCall map[int]struct { - result1 string - } - NamespaceStub func() string - namespaceMutex sync.RWMutex - namespaceArgsForCall []struct { - } - namespaceReturns struct { - result1 string - } - namespaceReturnsOnCall map[int]struct { - result1 string - } - NetworkStub func() string - networkMutex sync.RWMutex - networkArgsForCall []struct { - } - networkReturns struct { - result1 string - } - networkReturnsOnCall map[int]struct { - result1 string - } - RequestStub func() *token.Request - requestMutex sync.RWMutex - requestArgsForCall []struct { - } - requestReturns struct { - result1 *token.Request - } - requestReturnsOnCall map[int]struct { - result1 *token.Request - } - invocations map[string][][]interface{} - invocationsMutex sync.RWMutex -} - -func (fake *FakeTransaction) Channel() string { - fake.channelMutex.Lock() - ret, specificReturn := fake.channelReturnsOnCall[len(fake.channelArgsForCall)] - fake.channelArgsForCall = append(fake.channelArgsForCall, struct { - }{}) - stub := fake.ChannelStub - fakeReturns := fake.channelReturns - fake.recordInvocation("Channel", []interface{}{}) - fake.channelMutex.Unlock() - if stub != nil { - return stub() - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeTransaction) ChannelCallCount() int { - fake.channelMutex.RLock() - defer fake.channelMutex.RUnlock() - return len(fake.channelArgsForCall) -} - -func (fake *FakeTransaction) ChannelCalls(stub func() string) { - fake.channelMutex.Lock() - defer fake.channelMutex.Unlock() - fake.ChannelStub = stub -} - -func (fake *FakeTransaction) ChannelReturns(result1 string) { - fake.channelMutex.Lock() - defer fake.channelMutex.Unlock() - fake.ChannelStub = nil - fake.channelReturns = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) ChannelReturnsOnCall(i int, result1 string) { - fake.channelMutex.Lock() - defer fake.channelMutex.Unlock() - fake.ChannelStub = nil - if fake.channelReturnsOnCall == nil { - fake.channelReturnsOnCall = make(map[int]struct { - result1 string - }) - } - fake.channelReturnsOnCall[i] = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) ID() string { - fake.iDMutex.Lock() - ret, specificReturn := fake.iDReturnsOnCall[len(fake.iDArgsForCall)] - fake.iDArgsForCall = append(fake.iDArgsForCall, struct { - }{}) - stub := fake.IDStub - fakeReturns := fake.iDReturns - fake.recordInvocation("ID", []interface{}{}) - fake.iDMutex.Unlock() - if stub != nil { - return stub() - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeTransaction) IDCallCount() int { - fake.iDMutex.RLock() - defer fake.iDMutex.RUnlock() - return len(fake.iDArgsForCall) -} - -func (fake *FakeTransaction) IDCalls(stub func() string) { - fake.iDMutex.Lock() - defer fake.iDMutex.Unlock() - fake.IDStub = stub -} - -func (fake *FakeTransaction) IDReturns(result1 string) { - fake.iDMutex.Lock() - defer fake.iDMutex.Unlock() - fake.IDStub = nil - fake.iDReturns = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) IDReturnsOnCall(i int, result1 string) { - fake.iDMutex.Lock() - defer fake.iDMutex.Unlock() - fake.IDStub = nil - if fake.iDReturnsOnCall == nil { - fake.iDReturnsOnCall = make(map[int]struct { - result1 string - }) - } - fake.iDReturnsOnCall[i] = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) Namespace() string { - fake.namespaceMutex.Lock() - ret, specificReturn := fake.namespaceReturnsOnCall[len(fake.namespaceArgsForCall)] - fake.namespaceArgsForCall = append(fake.namespaceArgsForCall, struct { - }{}) - stub := fake.NamespaceStub - fakeReturns := fake.namespaceReturns - fake.recordInvocation("Namespace", []interface{}{}) - fake.namespaceMutex.Unlock() - if stub != nil { - return stub() - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeTransaction) NamespaceCallCount() int { - fake.namespaceMutex.RLock() - defer fake.namespaceMutex.RUnlock() - return len(fake.namespaceArgsForCall) -} - -func (fake *FakeTransaction) NamespaceCalls(stub func() string) { - fake.namespaceMutex.Lock() - defer fake.namespaceMutex.Unlock() - fake.NamespaceStub = stub -} - -func (fake *FakeTransaction) NamespaceReturns(result1 string) { - fake.namespaceMutex.Lock() - defer fake.namespaceMutex.Unlock() - fake.NamespaceStub = nil - fake.namespaceReturns = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) NamespaceReturnsOnCall(i int, result1 string) { - fake.namespaceMutex.Lock() - defer fake.namespaceMutex.Unlock() - fake.NamespaceStub = nil - if fake.namespaceReturnsOnCall == nil { - fake.namespaceReturnsOnCall = make(map[int]struct { - result1 string - }) - } - fake.namespaceReturnsOnCall[i] = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) Network() string { - fake.networkMutex.Lock() - ret, specificReturn := fake.networkReturnsOnCall[len(fake.networkArgsForCall)] - fake.networkArgsForCall = append(fake.networkArgsForCall, struct { - }{}) - stub := fake.NetworkStub - fakeReturns := fake.networkReturns - fake.recordInvocation("Network", []interface{}{}) - fake.networkMutex.Unlock() - if stub != nil { - return stub() - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeTransaction) NetworkCallCount() int { - fake.networkMutex.RLock() - defer fake.networkMutex.RUnlock() - return len(fake.networkArgsForCall) -} - -func (fake *FakeTransaction) NetworkCalls(stub func() string) { - fake.networkMutex.Lock() - defer fake.networkMutex.Unlock() - fake.NetworkStub = stub -} - -func (fake *FakeTransaction) NetworkReturns(result1 string) { - fake.networkMutex.Lock() - defer fake.networkMutex.Unlock() - fake.NetworkStub = nil - fake.networkReturns = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) NetworkReturnsOnCall(i int, result1 string) { - fake.networkMutex.Lock() - defer fake.networkMutex.Unlock() - fake.NetworkStub = nil - if fake.networkReturnsOnCall == nil { - fake.networkReturnsOnCall = make(map[int]struct { - result1 string - }) - } - fake.networkReturnsOnCall[i] = struct { - result1 string - }{result1} -} - -func (fake *FakeTransaction) Request() *token.Request { - fake.requestMutex.Lock() - ret, specificReturn := fake.requestReturnsOnCall[len(fake.requestArgsForCall)] - fake.requestArgsForCall = append(fake.requestArgsForCall, struct { - }{}) - stub := fake.RequestStub - fakeReturns := fake.requestReturns - fake.recordInvocation("Request", []interface{}{}) - fake.requestMutex.Unlock() - if stub != nil { - return stub() - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeTransaction) RequestCallCount() int { - fake.requestMutex.RLock() - defer fake.requestMutex.RUnlock() - return len(fake.requestArgsForCall) -} - -func (fake *FakeTransaction) RequestCalls(stub func() *token.Request) { - fake.requestMutex.Lock() - defer fake.requestMutex.Unlock() - fake.RequestStub = stub -} - -func (fake *FakeTransaction) RequestReturns(result1 *token.Request) { - fake.requestMutex.Lock() - defer fake.requestMutex.Unlock() - fake.RequestStub = nil - fake.requestReturns = struct { - result1 *token.Request - }{result1} -} - -func (fake *FakeTransaction) RequestReturnsOnCall(i int, result1 *token.Request) { - fake.requestMutex.Lock() - defer fake.requestMutex.Unlock() - fake.RequestStub = nil - if fake.requestReturnsOnCall == nil { - fake.requestReturnsOnCall = make(map[int]struct { - result1 *token.Request - }) - } - fake.requestReturnsOnCall[i] = struct { - result1 *token.Request - }{result1} -} - -func (fake *FakeTransaction) Invocations() map[string][][]interface{} { - fake.invocationsMutex.RLock() - defer fake.invocationsMutex.RUnlock() - copiedInvocations := map[string][][]interface{}{} - for key, value := range fake.invocations { - copiedInvocations[key] = value - } - return copiedInvocations -} - -func (fake *FakeTransaction) recordInvocation(key string, args []interface{}) { - fake.invocationsMutex.Lock() - defer fake.invocationsMutex.Unlock() - if fake.invocations == nil { - fake.invocations = map[string][][]interface{}{} - } - if fake.invocations[key] == nil { - fake.invocations[key] = [][]interface{}{} - } - fake.invocations[key] = append(fake.invocations[key], args) -} - -var _ tokens.Transaction = new(FakeTransaction) diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index b5b333aaa3..ad18074ec9 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -30,26 +30,6 @@ type MetaData interface { SpentTokenID() []*token2.ID } -// GetTMSProviderFunc is a function type that returns a token management service provider. -type GetTMSProviderFunc = func() *token.ManagementServiceProvider - -// UnspendableTokensIterator is an alias for the driver's UnsupportedTokensIterator. -type UnspendableTokensIterator = driver.UnsupportedTokensIterator - -// Transaction models a token transaction within Panurus, providing access to its identifiers and request content. -type Transaction interface { - // ID returns the transaction identifier. - ID() string - // Network returns the network name the transaction belongs to. - Network() string - // Channel returns the channel name. - Channel() string - // Namespace returns the namespace (chaincode ID) of the transaction. - Namespace() string - // Request returns the underlying token request. - Request() *token.Request -} - // Cache defines the interface for caching token requests and their extracted actions. type Cache interface { // Get retrieves a cache entry by key. diff --git a/token/services/tokens/tokens_coverage_test.go b/token/services/tokens/tokens_coverage_test.go new file mode 100644 index 0000000000..4331971069 --- /dev/null +++ b/token/services/tokens/tokens_coverage_test.go @@ -0,0 +1,198 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package tokens_test + +import ( + "context" + "testing" + + "github.com/LFDT-Panurus/panurus/token" + "github.com/LFDT-Panurus/panurus/token/services/storage/tokendb" + "github.com/LFDT-Panurus/panurus/token/services/tokens" + "github.com/LFDT-Panurus/panurus/token/services/tokens/mock" + token2 "github.com/LFDT-Panurus/panurus/token/token" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCacheRequest_ExtractActionsError verifies that when action extraction fails +// (here because the TMS provider errors), CacheRequest propagates the error and +// nothing is written to the cache. +func TestCacheRequest_ExtractActionsError(t *testing.T) { + ctx := context.Background() + + tmsProv := &mock.FakeTMSProvider{} + tmsProv.GetManagementServiceReturns(nil, assert.AnError) + cache := &mock.FakeCache{} + ts := &tokens.Service{TMSProvider: tmsProv, RequestsCache: cache} + + err := ts.CacheRequest(ctx, &token.Request{Anchor: "tx1"}) + require.Error(t, err) + require.ErrorContains(t, err, "failed to extract actions") + // A failed extraction must not leave a partial entry behind. + assert.Equal(t, 0, cache.AddCallCount()) +} + +// TestPruneInvalidUnspentTokens_GetManagementServiceError verifies that a failure +// to obtain the management service aborts the prune early with a wrapped error. +func TestPruneInvalidUnspentTokens_GetManagementServiceError(t *testing.T) { + ctx := context.Background() + + tmsProv := &mock.FakeTMSProvider{} + tmsProv.GetManagementServiceReturns(nil, assert.AnError) + ts := &tokens.Service{ + TMSProvider: tmsProv, + Storage: &tokens.DBStorage{TMSID: token.TMSID{Network: "net", Channel: "ch", Namespace: "ns"}}, + } + + deleted, err := ts.PruneInvalidUnspentTokens(ctx) + require.Error(t, err) + assert.ErrorContains(t, err, "failed getting token management service") + assert.Nil(t, deleted) + assert.Equal(t, 1, tmsProv.GetManagementServiceCallCount()) +} + +// TestDBStorage_ContinueTransaction verifies that ContinueTransaction wraps the +// transaction returned by the underlying store on success and propagates its error. +func TestDBStorage_ContinueTransaction(t *testing.T) { + tmsID := token.TMSID{Network: "net", Channel: "ch", Namespace: "ns"} + + t.Run("wraps the continued transaction on success", func(t *testing.T) { + pub := &mock.FakePublisher{} + mockTx := &mock.FakeTokenStoreTransaction{} + mockDB := &mock.FakeTokenStore{} + mockDB.ContinueTokenDBTransactionReturns(mockTx, nil) + storage := &tokens.DBStorage{Notifier: pub, TokenDB: &tokendb.StoreService{TokenStore: mockDB}, TMSID: tmsID} + + dbtx, err := storage.ContinueTransaction(nil) + require.NoError(t, err) + require.NotNil(t, dbtx) + assert.Equal(t, 1, mockDB.ContinueTokenDBTransactionCallCount()) + // The wrapper carries over the storage's notifier and TMS identifier. + assert.Equal(t, tmsID, dbtx.TMSID) + assert.Equal(t, pub, dbtx.Notifier) + }) + + t.Run("propagates the store error", func(t *testing.T) { + mockDB := &mock.FakeTokenStore{} + mockDB.ContinueTokenDBTransactionReturns(nil, assert.AnError) + storage := &tokens.DBStorage{TokenDB: &tokendb.StoreService{TokenStore: mockDB}, TMSID: tmsID} + + dbtx, err := storage.ContinueTransaction(nil) + require.Error(t, err) + assert.Nil(t, dbtx) + }) +} + +// TestTransaction_Commit verifies that Commit delegates to the underlying store +// transaction and passes its result (success or failure) straight through. +func TestTransaction_Commit(t *testing.T) { + tmsID := token.TMSID{Network: "net", Channel: "ch", Namespace: "ns"} + + t.Run("delegates to the underlying transaction", func(t *testing.T) { + mockTx := &mock.FakeTokenStoreTransaction{} + tx, err := tokens.NewTransaction(nil, &tokendb.Transaction{TokenStoreTransaction: mockTx}, tmsID) + require.NoError(t, err) + + require.NoError(t, tx.Commit()) + assert.Equal(t, 1, mockTx.CommitCallCount()) + }) + + t.Run("propagates a commit failure", func(t *testing.T) { + mockTx := &mock.FakeTokenStoreTransaction{} + mockTx.CommitReturns(assert.AnError) + tx, err := tokens.NewTransaction(nil, &tokendb.Transaction{TokenStoreTransaction: mockTx}, tmsID) + require.NoError(t, err) + + err = tx.Commit() + require.ErrorIs(t, err, assert.AnError) + assert.Equal(t, 1, mockTx.CommitCallCount()) + }) +} + +// TestParse_GraphHiding verifies that with graph hiding enabled the spent inputs +// come from the request metadata (SpentTokenID) rather than from the input stream. +func TestParse_GraphHiding(t *testing.T) { + ctx := context.Background() + ts := &tokens.Service{Storage: &tokens.DBStorage{}} + + spentIDs := []*token2.ID{{TxId: "s1", Index: 0}, {TxId: "s2", Index: 1}} + md := &mock.FakeMetaData{} + md.SpentTokenIDReturns(spentIDs) + + qs := &mock.FakeQueryService{} + is := token.NewInputStream(qs, []*token.Input{}, 64) + os := token.NewOutputStream([]*token.Output{}, 64) + auth := &mock.FakeAuthorization{} + + spend, store, err := ts.Parse(ctx, auth, "tx1", md, is, os, false, 64, true) + require.NoError(t, err) + assert.Equal(t, spentIDs, spend) + assert.Empty(t, store) + assert.Equal(t, 1, md.SpentTokenIDCallCount()) +} + +// TestParse_SkipsNilInputAndForeignOutput verifies the two skip branches of Parse: +// an input that is not mine (nil Id) is not marked spent, and an output that is +// neither mine, audited, nor issued is discarded rather than stored. +func TestParse_SkipsNilInputAndForeignOutput(t *testing.T) { + ctx := context.Background() + ts := &tokens.Service{Storage: &tokens.DBStorage{}} + md := &mock.FakeMetaData{} + + qs := &mock.FakeQueryService{} + is := token.NewInputStream(qs, []*token.Input{{Id: nil}}, 64) + + foreign := &token.Output{ + Token: token2.Token{Type: "TOK", Owner: []byte("carol")}, + Index: 0, + Type: "TOK", + LedgerOutput: []byte("carol,TOK,0x0"), + Quantity: token2.NewQuantityFromUInt64(10), + } + os := token.NewOutputStream([]*token.Output{foreign}, 64) + + auth := &mock.FakeAuthorization{} + auth.IsMineStub = func(context.Context, *token2.Token) (string, []string, bool) { + return "", nil, false + } + auth.IssuedReturns(false) + + spend, store, err := ts.Parse(ctx, auth, "tx1", md, is, os, false, 64, false) + require.NoError(t, err) + assert.Empty(t, spend) + assert.Empty(t, store) +} + +// TestParse_OwnerTypeError verifies that a failure to resolve the owner type of an +// otherwise-kept output aborts Parse with a wrapped error. +func TestParse_OwnerTypeError(t *testing.T) { + ctx := context.Background() + ts := &tokens.Service{Storage: &tokens.DBStorage{}} + md := &mock.FakeMetaData{} + + owned := &token.Output{ + Token: token2.Token{Type: "TOK", Owner: []byte("alice")}, + Index: 0, + Type: "TOK", + LedgerOutput: []byte("alice,TOK,0x0"), + Quantity: token2.NewQuantityFromUInt64(10), + } + qs := &mock.FakeQueryService{} + is := token.NewInputStream(qs, []*token.Input{}, 64) + os := token.NewOutputStream([]*token.Output{owned}, 64) + + auth := &mock.FakeAuthorization{} + auth.IsMineStub = func(context.Context, *token2.Token) (string, []string, bool) { + return "wallet", []string{"alice"}, true + } + auth.OwnerTypeReturns(0, nil, assert.AnError) + + _, _, err := ts.Parse(ctx, auth, "tx1", md, is, os, false, 64, false) + require.Error(t, err) + assert.ErrorContains(t, err, "failed to extract owner type") +} diff --git a/token/services/tokens/tokens_internal_test.go b/token/services/tokens/tokens_internal_test.go new file mode 100644 index 0000000000..3db3f83cb8 --- /dev/null +++ b/token/services/tokens/tokens_internal_test.go @@ -0,0 +1,110 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package tokens + +import ( + "context" + "testing" + + "github.com/LFDT-Panurus/panurus/token" + token2 "github.com/LFDT-Panurus/panurus/token/token" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The functions exercised here (getActions, extractActions, deleteTokens) are +// unexported, so these tests live in the internal `tokens` package. That rules +// out the counterfeiter mocks (the mock package imports tokens, which would form +// an import cycle), so the tiny Cache and TMSProvider interfaces are stubbed by +// hand below. + +// stubCache is a minimal, hand-rolled implementation of the Cache interface that +// records the keys it is queried with and how often it is mutated. +type stubCache struct { + entry *CacheEntry + ok bool + getKeys []string + adds int + deletes int +} + +func (c *stubCache) Get(key string) (*CacheEntry, bool) { + c.getKeys = append(c.getKeys, key) + + return c.entry, c.ok +} + +func (c *stubCache) Add(string, *CacheEntry) { c.adds++ } + +func (c *stubCache) Delete(string) { c.deletes++ } + +// errTMSProvider is a TMSProvider whose GetManagementService always fails, used +// to drive the error branches of extractActions/getActions without constructing +// a real *token.ManagementService. +type errTMSProvider struct{ err error } + +func (p errTMSProvider) GetManagementService(...token.ServiceOption) (*token.ManagementService, error) { + return nil, p.err +} + +// TestGetActions_CacheHit verifies that a populated cache entry short-circuits +// getActions: it returns the cached spend/append sets without touching the TMS. +func TestGetActions_CacheHit(t *testing.T) { + ctx := context.Background() + + spend := []*token2.ID{{TxId: "in", Index: 0}} + appendTokens := []TokenToAppend{{TxID: "tx1", Index: 0}} + cache := &stubCache{entry: &CacheEntry{ToSpend: spend, ToAppend: appendTokens}, ok: true} + + // TMSProvider intentionally nil: a cache hit must not reach it. + s := &Service{RequestsCache: cache} + + gotSpend, gotAppend, err := s.getActions(ctx, "tx1", nil) + require.NoError(t, err) + assert.Equal(t, spend, gotSpend) + assert.Equal(t, appendTokens, gotAppend) + assert.Equal(t, []string{"tx1"}, cache.getKeys) +} + +// TestGetActions_CacheMiss_DelegatesToExtractActions verifies that a cache miss +// falls through to extractActions, so a TMS failure there surfaces to the caller. +func TestGetActions_CacheMiss_DelegatesToExtractActions(t *testing.T) { + ctx := context.Background() + + cache := &stubCache{ok: false} + s := &Service{RequestsCache: cache, TMSProvider: errTMSProvider{err: assert.AnError}} + + _, _, err := s.getActions(ctx, "tx1", &token.Request{Anchor: "tx1"}) + require.Error(t, err) + require.ErrorContains(t, err, "failed getting token management service") + assert.Equal(t, []string{"tx1"}, cache.getKeys) +} + +// TestExtractActions_TMSProviderError verifies that extractActions wraps and +// returns the error from the very first dependency it consults, the TMS provider. +func TestExtractActions_TMSProviderError(t *testing.T) { + ctx := context.Background() + + s := &Service{TMSProvider: errTMSProvider{err: assert.AnError}} + + _, _, err := s.extractActions(ctx, "tx1", &token.Request{Anchor: "tx1"}) + require.Error(t, err) + assert.ErrorContains(t, err, "failed getting token management service") +} + +// TestDeleteTokens_EmptyInput verifies the early return of deleteTokens: with no +// tokens to inspect it reports nothing deleted and never touches the network or +// TMS (both passed as nil here). +func TestDeleteTokens_EmptyInput(t *testing.T) { + ctx := context.Background() + + s := &Service{} + + ids, err := s.deleteTokens(ctx, nil, nil, nil) + require.NoError(t, err) + assert.Nil(t, ids) +} diff --git a/token/services/tokens/tokens_test.go b/token/services/tokens/tokens_test.go index 2e49a76499..167ab67a63 100644 --- a/token/services/tokens/tokens_test.go +++ b/token/services/tokens/tokens_test.go @@ -12,6 +12,7 @@ import ( "github.com/LFDT-Panurus/panurus/token" "github.com/LFDT-Panurus/panurus/token/driver" + "github.com/LFDT-Panurus/panurus/token/services/storage/tokendb" "github.com/LFDT-Panurus/panurus/token/services/tokens" "github.com/LFDT-Panurus/panurus/token/services/tokens/mock" token2 "github.com/LFDT-Panurus/panurus/token/token" @@ -157,6 +158,128 @@ func TestParse(t *testing.T) { assert.Equal(t, output2.Type, store[1].Tok.Type) } +// TestAppendValid_SkipsWhenNoRequestOrMetadata verifies that AppendValid is a no-op +// (returns nil without touching storage or the cache) when there is nothing to apply: +// either the request itself or its metadata is absent. +func TestAppendValid_SkipsWhenNoRequestOrMetadata(t *testing.T) { + ctx := context.Background() + + t.Run("nil request", func(t *testing.T) { + cache := &mock.FakeCache{} + ts := &tokens.Service{Storage: &tokens.DBStorage{}, RequestsCache: cache} + + require.NoError(t, ts.AppendValid(ctx, nil, "tx1", nil)) + // getActions was never reached, so the cache was neither consulted nor invalidated. + assert.Equal(t, 0, cache.GetCallCount()) + assert.Equal(t, 0, cache.DeleteCallCount()) + }) + + t.Run("nil metadata", func(t *testing.T) { + cache := &mock.FakeCache{} + ts := &tokens.Service{Storage: &tokens.DBStorage{}, RequestsCache: cache} + + req := &token.Request{Anchor: "tx1", Metadata: nil} + require.NoError(t, ts.AppendValid(ctx, nil, "tx1", req)) + assert.Equal(t, 0, cache.GetCallCount()) + assert.Equal(t, 0, cache.DeleteCallCount()) + }) +} + +// TestAppendValid_SkipsWhenTransactionExists verifies that a transaction already recorded +// in local storage is not processed a second time: AppendValid returns without extracting +// actions from the request or invalidating its cache entry. +func TestAppendValid_SkipsWhenTransactionExists(t *testing.T) { + ctx := context.Background() + + mockDB := &mock.FakeTokenStore{} + mockDB.TransactionExistsReturns(true, nil) + cache := &mock.FakeCache{} + storage := &tokens.DBStorage{TokenDB: &tokendb.StoreService{TokenStore: mockDB}} + ts := &tokens.Service{Storage: storage, RequestsCache: cache} + + req := &token.Request{Anchor: "tx1", Metadata: &driver.TokenRequestMetadata{}} + require.NoError(t, ts.AppendValid(ctx, nil, "tx1", req)) + + assert.Equal(t, 1, mockDB.TransactionExistsCallCount()) + // getActions must not run for an already-known transaction. + assert.Equal(t, 0, cache.GetCallCount()) + assert.Equal(t, 0, cache.DeleteCallCount()) +} + +// TestAppendValid_TransactionExistsError verifies that a storage failure while checking +// for an existing transaction is propagated to the caller rather than swallowed. +func TestAppendValid_TransactionExistsError(t *testing.T) { + ctx := context.Background() + + mockDB := &mock.FakeTokenStore{} + mockDB.TransactionExistsReturns(false, assert.AnError) + storage := &tokens.DBStorage{TokenDB: &tokendb.StoreService{TokenStore: mockDB}} + ts := &tokens.Service{Storage: storage, RequestsCache: &mock.FakeCache{}} + + req := &token.Request{Anchor: "tx1", Metadata: &driver.TokenRequestMetadata{}} + err := ts.AppendValid(ctx, nil, "tx1", req) + assert.ErrorIs(t, err, assert.AnError) +} + +// TestGetCachedTokenRequest verifies that a cached request is returned together with its +// serialized message on a hit, and that a miss yields two nil values. +func TestGetCachedTokenRequest(t *testing.T) { + cache := &mock.FakeCache{} + ts := &tokens.Service{RequestsCache: cache} + + // miss: nothing cached under this key + cache.GetReturns(nil, false) + req, msg := ts.GetCachedTokenRequest("missing") + assert.Nil(t, req) + assert.Nil(t, msg) + + // hit: the stored request and its message-to-sign are returned + want := &token.Request{Anchor: "tx1"} + cache.GetReturns(&tokens.CacheEntry{Request: want, MsgToSign: []byte("sig")}, true) + req, msg = ts.GetCachedTokenRequest("tx1") + assert.Same(t, want, req) + assert.Equal(t, []byte("sig"), msg) +} + +// TestSetSpendableFlag verifies that SetSpendableFlag commits the transaction on success and +// rolls it back (propagating the error) when the underlying store rejects the update. +func TestSetSpendableFlag(t *testing.T) { + ctx := context.Background() + tmsID := token.TMSID{Network: "net", Channel: "ch", Namespace: "ns"} + + t.Run("commits when the store succeeds", func(t *testing.T) { + mockTx := &mock.FakeTokenStoreTransaction{} + mockDB := &mock.FakeTokenStore{} + mockDB.NewTokenDBTransactionReturns(mockTx, nil) + storage := &tokens.DBStorage{TokenDB: &tokendb.StoreService{TokenStore: mockDB}, TMSID: tmsID} + ts := &tokens.Service{Storage: storage} + + id := &token2.ID{TxId: "tx1", Index: 0} + require.NoError(t, ts.SetSpendableFlag(ctx, true, id)) + + require.Equal(t, 1, mockTx.SetSpendableCallCount()) + assert.Equal(t, 1, mockTx.CommitCallCount()) + assert.Equal(t, 0, mockTx.RollbackCallCount()) + _, gotID, gotVal := mockTx.SetSpendableArgsForCall(0) + assert.Equal(t, *id, gotID) + assert.True(t, gotVal) + }) + + t.Run("rolls back when the store fails", func(t *testing.T) { + mockTx := &mock.FakeTokenStoreTransaction{} + mockTx.SetSpendableReturns(assert.AnError) + mockDB := &mock.FakeTokenStore{} + mockDB.NewTokenDBTransactionReturns(mockTx, nil) + storage := &tokens.DBStorage{TokenDB: &tokendb.StoreService{TokenStore: mockDB}, TMSID: tmsID} + ts := &tokens.Service{Storage: storage} + + err := ts.SetSpendableFlag(ctx, true, &token2.ID{TxId: "tx1", Index: 0}) + require.Error(t, err) + assert.Equal(t, 1, mockTx.RollbackCallCount()) + assert.Equal(t, 0, mockTx.CommitCallCount()) + }) +} + // TestParseRedeem verifies that a redeem output (empty owner) is stored as a redeemed token // when its issuer is known to this node, and skipped otherwise. func TestParseRedeem(t *testing.T) {