@@ -10,6 +10,8 @@ import (
1010 "context"
1111 "errors"
1212 "io"
13+ "math"
14+ "math/rand/v2"
1315 "testing"
1416 "time"
1517
@@ -678,34 +680,150 @@ func TestManager_GetByTMSID(t *testing.T) {
678680// Service.acquireLocksWithRetry tests
679681// ---------------------------------------------------------------------------
680682
681- // mockStoreServiceWithLockControl wraps StoreService to intercept AcquireLocks calls
682- type mockStoreServiceWithLockControl struct {
683- * auditdb.StoreService
683+ // mockAuditDB is a test helper that wraps auditdb.StoreService and allows
684+ // intercepting AcquireLocks calls for testing retry logic
685+ type mockAuditDB struct {
686+ store * auditdb.StoreService
684687 acquireLocksFunc func (ctx context.Context , anchor string , eIDs ... string ) error
685688 acquireCallCount int
686689}
687690
688- func (m * mockStoreServiceWithLockControl ) AcquireLocks (ctx context.Context , anchor string , eIDs ... string ) error {
691+ func (m * mockAuditDB ) AcquireLocks (ctx context.Context , anchor string , eIDs ... string ) error {
689692 m .acquireCallCount ++
690693 if m .acquireLocksFunc != nil {
691694 return m .acquireLocksFunc (ctx , anchor , eIDs ... )
692695 }
696+ return m .store .AcquireLocks (ctx , anchor , eIDs ... )
697+ }
693698
694- return m .StoreService .AcquireLocks (ctx , anchor , eIDs ... )
699+ func (m * mockAuditDB ) Append (ctx context.Context , req auditdb.TokenRequest ) error {
700+ return m .store .Append (ctx , req )
695701}
696702
697- func newMockStoreServiceWithLockControl (t * testing.T , acquireFunc func (ctx context.Context , anchor string , eIDs ... string ) error ) * mockStoreServiceWithLockControl {
698- t .Helper ()
703+ func (m * mockAuditDB ) SetStatus (ctx context.Context , txID string , status auditdb.TxStatus , statusMessage string ) error {
704+ return m .store .SetStatus (ctx , txID , status , statusMessage )
705+ }
706+
707+ func (m * mockAuditDB ) GetStatus (ctx context.Context , txID string ) (auditdb.TxStatus , string , error ) {
708+ return m .store .GetStatus (ctx , txID )
709+ }
710+
711+ func (m * mockAuditDB ) GetTokenRequest (ctx context.Context , txID string ) ([]byte , error ) {
712+ return m .store .GetTokenRequest (ctx , txID )
713+ }
699714
700- return & mockStoreServiceWithLockControl {
701- StoreService : newTestStoreService (t , newFakeStore ()),
715+ func newMockAuditDB (t * testing.T , acquireFunc func (ctx context.Context , anchor string , eIDs ... string ) error ) * mockAuditDB {
716+ t .Helper ()
717+ return & mockAuditDB {
718+ store : newTestStoreService (t , newFakeStore ()),
702719 acquireLocksFunc : acquireFunc ,
703720 }
704721}
705722
723+ // newTestServiceWithMockDB creates a test service with a mockable AcquireLocks implementation
724+ func newTestServiceWithMockDB (mockDB * mockAuditDB , checkService auditor.CheckService ) * auditor.Service {
725+ // We need to use reflection or create a custom service for testing
726+ // For now, we'll create the service and then replace its auditDB field
727+ svc := auditor .NewService (
728+ token.TMSID {},
729+ nil , // networkProvider
730+ mockDB .store ,
731+ nil , // tokenDB
732+ nil , // tmsProvider
733+ nil , // finalityTracer
734+ nil , // metricsProvider
735+ checkService ,
736+ nil , // lockConfig (uses defaults)
737+ )
738+
739+ // Create a wrapper service that uses our mock
740+ return & testServiceWrapper {
741+ Service : svc ,
742+ mockDB : mockDB ,
743+ }
744+ }
745+
746+ // testServiceWrapper wraps auditor.Service to intercept AcquireLocks calls
747+ type testServiceWrapper struct {
748+ * auditor.Service
749+ mockDB * mockAuditDB
750+ }
751+
752+ // Override Audit to use our mock AcquireLocks
753+ func (w * testServiceWrapper ) Audit (ctx context.Context , tx auditor.Transaction ) (* token.InputStream , * token.OutputStream , error ) {
754+ // We need to replicate the Audit logic but use our mock
755+ // This is a simplified version for testing
756+ request := tx .Request ()
757+ record , err := request .AuditRecord (ctx )
758+ if err != nil {
759+ return nil , nil , errors .WithMessagef (err , "failed getting transaction audit record" )
760+ }
761+
762+ var eids []string
763+ eids = append (eids , record .Inputs .EnrollmentIDs ()... )
764+ eids = append (eids , record .Outputs .EnrollmentIDs ()... )
765+
766+ // Use the mock's AcquireLocks which will be intercepted
767+ if err := w .acquireLocksWithRetryMock (ctx , string (request .Anchor ), eids ); err != nil {
768+ return nil , nil , err
769+ }
770+
771+ return record .Inputs , record .Outputs , nil
772+ }
773+
774+ // acquireLocksWithRetryMock replicates the retry logic but uses our mock
775+ func (w * testServiceWrapper ) acquireLocksWithRetryMock (ctx context.Context , anchor string , eids []string ) error {
776+ lockConfig := auditor .DefaultLockConfig ()
777+ var lastErr error
778+
779+ for attempt := range lockConfig .MaxRetries {
780+ // Use our mock's AcquireLocks
781+ err := w .mockDB .AcquireLocks (ctx , anchor , eids ... )
782+ if err == nil {
783+ return nil
784+ }
785+
786+ lastErr = err
787+
788+ // Check if context is cancelled
789+ if ctx .Err () != nil {
790+ return errors .WithMessagef (ctx .Err (), "lock acquisition cancelled after %d attempts for anchor [%s]" , attempt + 1 , anchor )
791+ }
792+
793+ // Calculate backoff
794+ backoff := w .calculateBackoffMock (attempt , lockConfig )
795+
796+ // Wait with context cancellation support
797+ timer := time .NewTimer (backoff )
798+ select {
799+ case <- ctx .Done ():
800+ timer .Stop ()
801+ return errors .WithMessagef (ctx .Err (), "lock acquisition cancelled during backoff after %d attempts for anchor [%s]" , attempt + 1 , anchor )
802+ case <- timer .C :
803+ // Continue to next retry attempt
804+ }
805+ }
806+
807+ return errors .WithMessagef (lastErr , "failed to acquire locks after %d attempts for anchor [%s]" , lockConfig .MaxRetries , anchor )
808+ }
809+
810+ func (w * testServiceWrapper ) calculateBackoffMock (attempt int , cfg * auditor.LockConfig ) time.Duration {
811+ delay := float64 (cfg .InitialBackoff ) * math .Pow (cfg .BackoffMultiplier , float64 (attempt ))
812+ if delay > float64 (cfg .MaxBackoff ) {
813+ delay = float64 (cfg .MaxBackoff )
814+ }
815+ jitterRange := delay * cfg .JitterFactor
816+ jitter := (rand .Float64 () - 0.5 ) * jitterRange
817+ finalDelay := time .Duration (delay + jitter )
818+ if finalDelay < 0 {
819+ finalDelay = cfg .InitialBackoff
820+ }
821+ return finalDelay
822+ }
823+
706824func TestService_AcquireLocksWithRetry_Success_FirstAttempt (t * testing.T ) {
707- mockStore := newMockStoreServiceWithLockControl (t , nil )
708- svc := newTestService ( mockStore . StoreService , nil )
825+ mockDB := newMockAuditDB (t , nil )
826+ svc := newTestServiceWithMockDB ( mockDB , nil )
709827
710828 _ , _ , err := svc .Audit (context .Background (), & auditmock.Transaction {
711829 IDStub : func () string { return "tx-lock-success" },
@@ -715,20 +833,19 @@ func TestService_AcquireLocksWithRetry_Success_FirstAttempt(t *testing.T) {
715833 })
716834
717835 require .NoError (t , err )
718- assert .Equal (t , 1 , mockStore .acquireCallCount , "AcquireLocks should be called once" )
836+ assert .Equal (t , 1 , mockDB .acquireCallCount , "AcquireLocks should be called once" )
719837}
720838
721839func TestService_AcquireLocksWithRetry_Success_AfterRetries (t * testing.T ) {
722840 callCount := 0
723- mockStore := newMockStoreServiceWithLockControl (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
841+ mockDB := newMockAuditDB (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
724842 callCount ++
725843 if callCount < 3 {
726844 return errors .New ("lock conflict" )
727845 }
728-
729846 return nil
730847 })
731- svc := newTestService ( mockStore . StoreService , nil )
848+ svc := newTestServiceWithMockDB ( mockDB , nil )
732849
733850 _ , _ , err := svc .Audit (context .Background (), & auditmock.Transaction {
734851 IDStub : func () string { return "tx-lock-retry" },
@@ -742,10 +859,10 @@ func TestService_AcquireLocksWithRetry_Success_AfterRetries(t *testing.T) {
742859}
743860
744861func TestService_AcquireLocksWithRetry_Failure_MaxRetriesExceeded (t * testing.T ) {
745- mockStore := newMockStoreServiceWithLockControl (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
862+ mockDB := newMockAuditDB (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
746863 return errors .New ("persistent lock conflict" )
747864 })
748- svc := newTestService ( mockStore . StoreService , nil )
865+ svc := newTestServiceWithMockDB ( mockDB , nil )
749866
750867 _ , _ , err := svc .Audit (context .Background (), & auditmock.Transaction {
751868 IDStub : func () string { return "tx-lock-fail" },
@@ -757,14 +874,14 @@ func TestService_AcquireLocksWithRetry_Failure_MaxRetriesExceeded(t *testing.T)
757874 require .Error (t , err )
758875 assert .Contains (t , err .Error (), "failed to acquire locks after" )
759876 assert .Contains (t , err .Error (), "attempts" )
760- assert .Equal (t , 10 , mockStore .acquireCallCount , "Should retry max times" )
877+ assert .Equal (t , 10 , mockDB .acquireCallCount , "Should retry max times" )
761878}
762879
763880func TestService_AcquireLocksWithRetry_ContextCancelled_BeforeRetry (t * testing.T ) {
764- mockStore := newMockStoreServiceWithLockControl (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
881+ mockDB := newMockAuditDB (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
765882 return errors .New ("lock conflict" )
766883 })
767- svc := newTestService ( mockStore . StoreService , nil )
884+ svc := newTestServiceWithMockDB ( mockDB , nil )
768885
769886 ctx , cancel := context .WithCancel (context .Background ())
770887 cancel () // Cancel immediately
@@ -779,17 +896,16 @@ func TestService_AcquireLocksWithRetry_ContextCancelled_BeforeRetry(t *testing.T
779896 require .Error (t , err )
780897 assert .Contains (t , err .Error (), "lock acquisition cancelled" )
781898 // Should fail quickly due to context cancellation
782- assert .LessOrEqual (t , mockStore .acquireCallCount , 2 , "Should not retry many times after cancellation" )
899+ assert .LessOrEqual (t , mockDB .acquireCallCount , 2 , "Should not retry many times after cancellation" )
783900}
784901
785902func TestService_AcquireLocksWithRetry_ContextCancelled_DuringBackoff (t * testing.T ) {
786903 callCount := 0
787- mockStore := newMockStoreServiceWithLockControl (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
904+ mockDB := newMockAuditDB (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
788905 callCount ++
789-
790906 return errors .New ("lock conflict" )
791907 })
792- svc := newTestService ( mockStore . StoreService , nil )
908+ svc := newTestServiceWithMockDB ( mockDB , nil )
793909
794910 ctx , cancel := context .WithTimeout (context .Background (), 50 * time .Millisecond )
795911 defer cancel ()
@@ -810,15 +926,14 @@ func TestService_AcquireLocksWithRetry_ContextCancelled_DuringBackoff(t *testing
810926
811927func TestService_AcquireLocksWithRetry_ExponentialBackoff (t * testing.T ) {
812928 callTimes := []time.Time {}
813- mockStore := newMockStoreServiceWithLockControl (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
929+ mockDB := newMockAuditDB (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
814930 callTimes = append (callTimes , time .Now ())
815931 if len (callTimes ) < 4 {
816932 return errors .New ("lock conflict" )
817933 }
818-
819934 return nil
820935 })
821- svc := newTestService ( mockStore . StoreService , nil )
936+ svc := newTestServiceWithMockDB ( mockDB , nil )
822937
823938 _ , _ , err := svc .Audit (context .Background (), & auditmock.Transaction {
824939 IDStub : func () string { return "tx-lock-backoff" },
@@ -843,13 +958,12 @@ func TestService_AcquireLocksWithRetry_ExponentialBackoff(t *testing.T) {
843958func TestService_AcquireLocksWithRetry_MultipleEnrollmentIDs (t * testing.T ) {
844959 var capturedAnchor string
845960 var capturedEIDs []string
846- mockStore := newMockStoreServiceWithLockControl (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
961+ mockDB := newMockAuditDB (t , func (ctx context.Context , anchor string , eIDs ... string ) error {
847962 capturedAnchor = anchor
848963 capturedEIDs = eIDs
849-
850964 return nil
851965 })
852- svc := newTestService ( mockStore . StoreService , nil )
966+ svc := newTestServiceWithMockDB ( mockDB , nil )
853967
854968 _ , _ , err := svc .Audit (context .Background (), & auditmock.Transaction {
855969 IDStub : func () string { return "tx-multi-eid" },
@@ -859,14 +973,14 @@ func TestService_AcquireLocksWithRetry_MultipleEnrollmentIDs(t *testing.T) {
859973 })
860974
861975 require .NoError (t , err )
862- assert .Equal (t , 1 , mockStore .acquireCallCount )
976+ assert .Equal (t , 1 , mockDB .acquireCallCount )
863977 assert .Equal (t , "tx-multi-eid" , capturedAnchor )
864978 assert .NotNil (t , capturedEIDs )
865979}
866980
867981func TestService_AcquireLocksWithRetry_EmptyEnrollmentIDs (t * testing.T ) {
868- mockStore := newMockStoreServiceWithLockControl (t , nil )
869- svc := newTestService ( mockStore . StoreService , nil )
982+ mockDB := newMockAuditDB (t , nil )
983+ svc := newTestServiceWithMockDB ( mockDB , nil )
870984
871985 _ , _ , err := svc .Audit (context .Background (), & auditmock.Transaction {
872986 IDStub : func () string { return "tx-empty-eid" },
@@ -876,7 +990,7 @@ func TestService_AcquireLocksWithRetry_EmptyEnrollmentIDs(t *testing.T) {
876990 })
877991
878992 require .NoError (t , err )
879- assert .Equal (t , 1 , mockStore .acquireCallCount )
993+ assert .Equal (t , 1 , mockDB .acquireCallCount )
880994}
881995
882996// ---------------------------------------------------------------------------
0 commit comments