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