@@ -309,6 +309,63 @@ func TestService_Audit_Success(t *testing.T) {
309309 assert .NotNil (t , outputs )
310310}
311311
312+ // TestService_Audit_LockAcquisitionFailure verifies that when AcquireLocks() fails
313+ // due to context cancellation, the error is properly returned and no locks are held.
314+ func TestService_Audit_LockAcquisitionFailure (t * testing.T ) {
315+ // Create a StoreService with real lock mechanism
316+ storeService := newTestStoreService (t , newFakeStore ())
317+
318+ // First, acquire locks directly on enrollment IDs to simulate lock contention
319+ ctx := context .Background ()
320+ err := storeService .AcquireLocks (ctx , "blocking-anchor" , "test-eid-1" , "test-eid-2" )
321+ require .NoError (t , err )
322+ defer storeService .ReleaseLocks (ctx , "blocking-anchor" )
323+
324+ // Now try to acquire the same locks with a cancelled context
325+ cancelledCtx , cancel := context .WithCancel (context .Background ())
326+ cancel () // Cancel immediately to simulate timeout/cancellation
327+
328+ err = storeService .AcquireLocks (cancelledCtx , "tx-lock-fail" , "test-eid-1" , "test-eid-2" )
329+ require .Error (t , err )
330+ assert .Contains (t , err .Error (), "context canceled" )
331+
332+ // Verify no locks are held for the failed transaction by checking the anchor is not stored
333+ storeService .ReleaseLocks (ctx , "tx-lock-fail" ) // Should be a no-op since locks weren't acquired
334+ }
335+
336+ // TestService_Audit_ContextCancellationDuringAcquisition verifies that when context
337+ // is cancelled or times out during lock acquisition, the semaphore automatically rolls
338+ // back acquired locks and returns an error, allowing subsequent acquisitions to succeed.
339+ func TestService_Audit_ContextCancellationDuringAcquisition (t * testing.T ) {
340+ // Create a StoreService with real lock mechanism
341+ storeService := newTestStoreService (t , newFakeStore ())
342+
343+ // Acquire locks to create contention
344+ ctx := context .Background ()
345+ err := storeService .AcquireLocks (ctx , "blocking-anchor" , "test-eid-1" , "test-eid-2" )
346+ require .NoError (t , err )
347+
348+ // Create a context with a very short timeout
349+ timeoutCtx , cancel := context .WithTimeout (context .Background (), 1 )
350+ defer cancel ()
351+
352+ // This should fail due to context timeout while waiting for locks
353+ err = storeService .AcquireLocks (timeoutCtx , "tx-ctx-cancel" , "test-eid-1" , "test-eid-2" )
354+ require .Error (t , err )
355+ assert .True (t , errors .Is (err , context .DeadlineExceeded ) || errors .Is (err , context .Canceled ),
356+ "expected context cancellation error, got: %v" , err )
357+
358+ // Release the blocking locks
359+ storeService .ReleaseLocks (ctx , "blocking-anchor" )
360+
361+ // Verify the semaphore automatically rolled back by attempting acquisition with fresh context
362+ err = storeService .AcquireLocks (context .Background (), "tx-after-cancel" , "test-eid-1" , "test-eid-2" )
363+ require .NoError (t , err )
364+
365+ // Clean up
366+ storeService .ReleaseLocks (ctx , "tx-after-cancel" )
367+ }
368+
312369func TestService_Audit_DBCleanSuccess (t * testing.T ) {
313370 fakeStore := newFakeStore ()
314371 fakeStore .GetStatusReturns (0 , "" , errors .New ("db status err" ))
0 commit comments