@@ -341,3 +341,106 @@ func TestQuerySpentTokens_EmptyIDs(t *testing.T) {
341341 require .Nil (t , res )
342342 assert .Equal (t , 0 , qsp .GetCallCount ())
343343}
344+
345+ // TestQuerySpentTokens_NilIDPreservesAlignment is a regression guard: a nil id
346+ // must not shift the remaining flags or shorten the result. The returned slice
347+ // stays positionally aligned with ids (length == len(ids)); the nil position is
348+ // reported as not spent, and only the non-nil ids are looked up on the ledger.
349+ func TestQuerySpentTokens_NilIDPreservesAlignment (t * testing.T ) {
350+ ids := []* token.ID {nil , {TxId : "tx1" , Index : 0 }, {TxId : "tx2" , Index : 1 }}
351+ k1 := outputKey (t , "tx1" , 0 )
352+ k2 := outputKey (t , "tx2" , 1 )
353+
354+ qsp := & mock.QueryServiceProvider {}
355+ qs := & mock.QueryService {}
356+ qsp .GetReturns (qs , nil )
357+ // tx1 is present (unspent) -> false; tx2 is missing (spent) -> true.
358+ qs .GetStatesReturns (map [driver.Namespace ]map [driver.PKey ]driver.VaultValue {
359+ testNamespace : {k1 : {Raw : []byte ("token1" )}},
360+ }, nil )
361+
362+ e := newExecutor (qsp )
363+ res , err := e .QuerySpentTokens (t .Context (), testNamespace , ids , nil )
364+ require .NoError (t , err )
365+ // index 0 (nil) -> false, index 1 (tx1 present) -> false, index 2 (tx2 missing) -> true.
366+ require .Equal (t , []bool {false , false , true }, res )
367+
368+ // Only the non-nil ids are queried on the ledger; the nil id is not.
369+ queried := qs .GetStatesArgsForCall (0 )
370+ require .Equal (t , map [driver.Namespace ][]driver.PKey {testNamespace : {k1 , k2 }}, queried )
371+ }
372+
373+ // TestQuerySpentTokens_AllNilIDs is a regression guard: before the fix, an
374+ // all-nil ids slice returned (nil, nil) — length 0 — which made the callers'
375+ // spent[i] loop panic with index-out-of-range. It must now return a slice of
376+ // len(ids) with every position reported as not spent, without touching the
377+ // query service.
378+ func TestQuerySpentTokens_AllNilIDs (t * testing.T ) {
379+ ids := []* token.ID {nil , nil }
380+
381+ qsp := & mock.QueryServiceProvider {}
382+
383+ e := newExecutor (qsp )
384+ res , err := e .QuerySpentTokens (t .Context (), testNamespace , ids , nil )
385+ require .NoError (t , err )
386+ require .Equal (t , []bool {false , false }, res )
387+ assert .Equal (t , 0 , qsp .GetCallCount ())
388+ }
389+
390+ // TestQuerySpentTokens_InteriorNilPreservesAlignment guards the index mapping
391+ // for a nil that sits between two non-nil ids (not just a leading nil): the
392+ // flag for each non-nil id must land at that id's original position, and the
393+ // nil slot stays not spent.
394+ func TestQuerySpentTokens_InteriorNilPreservesAlignment (t * testing.T ) {
395+ ids := []* token.ID {{TxId : "tx1" , Index : 0 }, nil , {TxId : "tx2" , Index : 1 }}
396+ k1 := outputKey (t , "tx1" , 0 )
397+ k2 := outputKey (t , "tx2" , 1 )
398+
399+ qsp := & mock.QueryServiceProvider {}
400+ qs := & mock.QueryService {}
401+ qsp .GetReturns (qs , nil )
402+ // tx1 missing (spent) -> true; tx2 present (unspent) -> false.
403+ qs .GetStatesReturns (map [driver.Namespace ]map [driver.PKey ]driver.VaultValue {
404+ testNamespace : {k2 : {Raw : []byte ("token2" )}},
405+ }, nil )
406+
407+ e := newExecutor (qsp )
408+ res , err := e .QuerySpentTokens (t .Context (), testNamespace , ids , nil )
409+ require .NoError (t , err )
410+ // index 0 (tx1 missing) -> true, index 1 (nil) -> false, index 2 (tx2 present) -> false.
411+ require .Equal (t , []bool {true , false , false }, res )
412+
413+ // The nil id is not queried; only tx1 and tx2 are.
414+ queried := qs .GetStatesArgsForCall (0 )
415+ require .Equal (t , map [driver.Namespace ][]driver.PKey {testNamespace : {k1 , k2 }}, queried )
416+ }
417+
418+ func TestQuerySpentTokens_ProviderError (t * testing.T ) {
419+ ids := []* token.ID {{TxId : "tx1" , Index : 0 }}
420+
421+ qsp := & mock.QueryServiceProvider {}
422+ qsp .GetReturns (nil , errors .New ("boom" ))
423+
424+ e := newExecutor (qsp )
425+ res , err := e .QuerySpentTokens (t .Context (), testNamespace , ids , nil )
426+ require .Error (t , err )
427+ assert .Nil (t , res )
428+ assert .Contains (t , err .Error (), "failed getting qs" )
429+ assert .Contains (t , err .Error (), "boom" )
430+ }
431+
432+ func TestQuerySpentTokens_GetStatesError (t * testing.T ) {
433+ ids := []* token.ID {{TxId : "tx1" , Index : 0 }}
434+
435+ qsp := & mock.QueryServiceProvider {}
436+ qs := & mock.QueryService {}
437+ qsp .GetReturns (qs , nil )
438+ qs .GetStatesReturns (nil , errors .New ("rpc failed" ))
439+
440+ e := newExecutor (qsp )
441+ res , err := e .QuerySpentTokens (t .Context (), testNamespace , ids , nil )
442+ require .Error (t , err )
443+ assert .Nil (t , res )
444+ assert .Contains (t , err .Error (), "failed getting states" )
445+ assert .Contains (t , err .Error (), "rpc failed" )
446+ }
0 commit comments