@@ -12,6 +12,7 @@ import (
1212
1313 "github.com/LFDT-Panurus/panurus/token"
1414 "github.com/LFDT-Panurus/panurus/token/driver"
15+ "github.com/LFDT-Panurus/panurus/token/services/storage/tokendb"
1516 "github.com/LFDT-Panurus/panurus/token/services/tokens"
1617 "github.com/LFDT-Panurus/panurus/token/services/tokens/mock"
1718 token2 "github.com/LFDT-Panurus/panurus/token/token"
@@ -157,6 +158,128 @@ func TestParse(t *testing.T) {
157158 assert .Equal (t , output2 .Type , store [1 ].Tok .Type )
158159}
159160
161+ // TestAppendValid_SkipsWhenNoRequestOrMetadata verifies that AppendValid is a no-op
162+ // (returns nil without touching storage or the cache) when there is nothing to apply:
163+ // either the request itself or its metadata is absent.
164+ func TestAppendValid_SkipsWhenNoRequestOrMetadata (t * testing.T ) {
165+ ctx := context .Background ()
166+
167+ t .Run ("nil request" , func (t * testing.T ) {
168+ cache := & mock.FakeCache {}
169+ ts := & tokens.Service {Storage : & tokens.DBStorage {}, RequestsCache : cache }
170+
171+ require .NoError (t , ts .AppendValid (ctx , nil , "tx1" , nil ))
172+ // getActions was never reached, so the cache was neither consulted nor invalidated.
173+ assert .Equal (t , 0 , cache .GetCallCount ())
174+ assert .Equal (t , 0 , cache .DeleteCallCount ())
175+ })
176+
177+ t .Run ("nil metadata" , func (t * testing.T ) {
178+ cache := & mock.FakeCache {}
179+ ts := & tokens.Service {Storage : & tokens.DBStorage {}, RequestsCache : cache }
180+
181+ req := & token.Request {Anchor : "tx1" , Metadata : nil }
182+ require .NoError (t , ts .AppendValid (ctx , nil , "tx1" , req ))
183+ assert .Equal (t , 0 , cache .GetCallCount ())
184+ assert .Equal (t , 0 , cache .DeleteCallCount ())
185+ })
186+ }
187+
188+ // TestAppendValid_SkipsWhenTransactionExists verifies that a transaction already recorded
189+ // in local storage is not processed a second time: AppendValid returns without extracting
190+ // actions from the request or invalidating its cache entry.
191+ func TestAppendValid_SkipsWhenTransactionExists (t * testing.T ) {
192+ ctx := context .Background ()
193+
194+ mockDB := & mock.FakeTokenStore {}
195+ mockDB .TransactionExistsReturns (true , nil )
196+ cache := & mock.FakeCache {}
197+ storage := & tokens.DBStorage {TokenDB : & tokendb.StoreService {TokenStore : mockDB }}
198+ ts := & tokens.Service {Storage : storage , RequestsCache : cache }
199+
200+ req := & token.Request {Anchor : "tx1" , Metadata : & driver.TokenRequestMetadata {}}
201+ require .NoError (t , ts .AppendValid (ctx , nil , "tx1" , req ))
202+
203+ assert .Equal (t , 1 , mockDB .TransactionExistsCallCount ())
204+ // getActions must not run for an already-known transaction.
205+ assert .Equal (t , 0 , cache .GetCallCount ())
206+ assert .Equal (t , 0 , cache .DeleteCallCount ())
207+ }
208+
209+ // TestAppendValid_TransactionExistsError verifies that a storage failure while checking
210+ // for an existing transaction is propagated to the caller rather than swallowed.
211+ func TestAppendValid_TransactionExistsError (t * testing.T ) {
212+ ctx := context .Background ()
213+
214+ mockDB := & mock.FakeTokenStore {}
215+ mockDB .TransactionExistsReturns (false , assert .AnError )
216+ storage := & tokens.DBStorage {TokenDB : & tokendb.StoreService {TokenStore : mockDB }}
217+ ts := & tokens.Service {Storage : storage , RequestsCache : & mock.FakeCache {}}
218+
219+ req := & token.Request {Anchor : "tx1" , Metadata : & driver.TokenRequestMetadata {}}
220+ err := ts .AppendValid (ctx , nil , "tx1" , req )
221+ assert .ErrorIs (t , err , assert .AnError )
222+ }
223+
224+ // TestGetCachedTokenRequest verifies that a cached request is returned together with its
225+ // serialized message on a hit, and that a miss yields two nil values.
226+ func TestGetCachedTokenRequest (t * testing.T ) {
227+ cache := & mock.FakeCache {}
228+ ts := & tokens.Service {RequestsCache : cache }
229+
230+ // miss: nothing cached under this key
231+ cache .GetReturns (nil , false )
232+ req , msg := ts .GetCachedTokenRequest ("missing" )
233+ assert .Nil (t , req )
234+ assert .Nil (t , msg )
235+
236+ // hit: the stored request and its message-to-sign are returned
237+ want := & token.Request {Anchor : "tx1" }
238+ cache .GetReturns (& tokens.CacheEntry {Request : want , MsgToSign : []byte ("sig" )}, true )
239+ req , msg = ts .GetCachedTokenRequest ("tx1" )
240+ assert .Same (t , want , req )
241+ assert .Equal (t , []byte ("sig" ), msg )
242+ }
243+
244+ // TestSetSpendableFlag verifies that SetSpendableFlag commits the transaction on success and
245+ // rolls it back (propagating the error) when the underlying store rejects the update.
246+ func TestSetSpendableFlag (t * testing.T ) {
247+ ctx := context .Background ()
248+ tmsID := token.TMSID {Network : "net" , Channel : "ch" , Namespace : "ns" }
249+
250+ t .Run ("commits when the store succeeds" , func (t * testing.T ) {
251+ mockTx := & mock.FakeTokenStoreTransaction {}
252+ mockDB := & mock.FakeTokenStore {}
253+ mockDB .NewTokenDBTransactionReturns (mockTx , nil )
254+ storage := & tokens.DBStorage {TokenDB : & tokendb.StoreService {TokenStore : mockDB }, TMSID : tmsID }
255+ ts := & tokens.Service {Storage : storage }
256+
257+ id := & token2.ID {TxId : "tx1" , Index : 0 }
258+ require .NoError (t , ts .SetSpendableFlag (ctx , true , id ))
259+
260+ require .Equal (t , 1 , mockTx .SetSpendableCallCount ())
261+ assert .Equal (t , 1 , mockTx .CommitCallCount ())
262+ assert .Equal (t , 0 , mockTx .RollbackCallCount ())
263+ _ , gotID , gotVal := mockTx .SetSpendableArgsForCall (0 )
264+ assert .Equal (t , * id , gotID )
265+ assert .True (t , gotVal )
266+ })
267+
268+ t .Run ("rolls back when the store fails" , func (t * testing.T ) {
269+ mockTx := & mock.FakeTokenStoreTransaction {}
270+ mockTx .SetSpendableReturns (assert .AnError )
271+ mockDB := & mock.FakeTokenStore {}
272+ mockDB .NewTokenDBTransactionReturns (mockTx , nil )
273+ storage := & tokens.DBStorage {TokenDB : & tokendb.StoreService {TokenStore : mockDB }, TMSID : tmsID }
274+ ts := & tokens.Service {Storage : storage }
275+
276+ err := ts .SetSpendableFlag (ctx , true , & token2.ID {TxId : "tx1" , Index : 0 })
277+ require .Error (t , err )
278+ assert .Equal (t , 1 , mockTx .RollbackCallCount ())
279+ assert .Equal (t , 0 , mockTx .CommitCallCount ())
280+ })
281+ }
282+
160283// TestParseRedeem verifies that a redeem output (empty owner) is stored as a redeemed token
161284// when its issuer is known to this node, and skipped otherwise.
162285func TestParseRedeem (t * testing.T ) {
0 commit comments