@@ -606,6 +606,109 @@ final class AlamofireNetworkErrorHandlerTests: XCTestCase {
606606 // Then - no crashes should occur (especially no EXC_BREAKPOINT from array index out of bounds)
607607 wait ( for: [ expectation] , timeout: 5.0 )
608608 }
609+
610+ // MARK: - Deadlock Regression Test
611+
612+ func test_no_deadlock_when_kvo_observer_triggers_during_flagSiteAsUnsupported( ) {
613+ // This test reproduces the exact deadlock scenario from the production crash:
614+ // 1. flagSiteAsUnsupported writes to UserDefaults
615+ // 2. UserDefaults triggers KVO notification synchronously
616+ // 3. KVO observer calls prepareAppPasswordSupport
617+ // 4. prepareAppPasswordSupport accesses appPasswordFailures
618+ //
619+ // BEFORE FIX: This would deadlock because:
620+ // - flagSiteAsUnsupported used queue.sync(flags: .barrier) around UserDefaults write
621+ // - KVO fired synchronously during the barrier
622+ // - prepareAppPasswordSupport tried queue.sync while barrier was still active
623+ //
624+ // AFTER FIX: No deadlock because:
625+ // - flagSiteAsUnsupported uses userDefaultsQueue.async for UserDefaults write
626+ // - KVO fires on userDefaultsQueue, not the main queue
627+ // - prepareAppPasswordSupport can safely use queue.sync on the main queue
628+
629+ // Given - Set up KVO observer to simulate the production scenario
630+ let siteID : Int64 = 12345
631+ let kvoTriggered = XCTestExpectation ( description: " KVO observer triggered " )
632+ let preparePasswordSupportCalled = XCTestExpectation ( description: " prepareAppPasswordSupport called from KVO " )
633+ let operationCompleted = XCTestExpectation ( description: " Operation completed without deadlock " )
634+
635+ var kvoObservation : NSKeyValueObservation ?
636+ kvoObservation = userDefaults. observe ( \. applicationPasswordUnsupportedList, options: [ . new] ) { [ weak self] _, _ in
637+ kvoTriggered. fulfill ( )
638+
639+ // Simulate what happens in production:
640+ // AlamofireNetwork.observeSelectedSite gets triggered by KVO
641+ // and calls prepareAppPasswordSupport
642+ self ? . errorHandler. prepareAppPasswordSupport ( for: siteID)
643+ preparePasswordSupportCalled. fulfill ( )
644+ }
645+
646+ // When - Trigger the scenario that caused the deadlock
647+ DispatchQueue . global ( ) . async {
648+ // This will write to UserDefaults, triggering KVO
649+ self . errorHandler. flagSiteAsUnsupported (
650+ for: siteID,
651+ flow: . apiRequest,
652+ cause: . majorError,
653+ error: NetworkError . notFound ( response: nil )
654+ )
655+ operationCompleted. fulfill ( )
656+ }
657+
658+ // Then - All expectations should complete without timing out (no deadlock)
659+ // The timeout of 2 seconds is generous - if there's a deadlock, this will timeout
660+ let result = XCTWaiter . wait (
661+ for: [ kvoTriggered, preparePasswordSupportCalled, operationCompleted] ,
662+ timeout: 2.0 ,
663+ enforceOrder: false
664+ )
665+
666+ XCTAssertEqual ( result, . completed, " Test should complete without deadlock. If this times out, the deadlock bug has returned! " )
667+
668+ // Cleanup
669+ kvoObservation? . invalidate ( )
670+ }
671+
672+ func test_no_deadlock_with_concurrent_kvo_observers_and_flag_operations( ) {
673+ // This test creates even more stress by having multiple KVO observers
674+ // and concurrent flag operations to ensure the fix is robust
675+
676+ let completionExpectation = XCTestExpectation ( description: " All operations complete " )
677+ completionExpectation. expectedFulfillmentCount = 10 // 10 flag operations
678+
679+ var observations : [ NSKeyValueObservation ] = [ ]
680+
681+ // Set up multiple KVO observers (simulating multiple parts of the app observing)
682+ for i in 1 ... 3 {
683+ let observation = userDefaults. observe ( \. applicationPasswordUnsupportedList, options: [ . new] ) { [ weak self] _, _ in
684+ let siteID = Int64 ( 1000 + i)
685+ // Each observer tries to access the error handler
686+ self ? . errorHandler. prepareAppPasswordSupport ( for: siteID)
687+ }
688+ observations. append ( observation)
689+ }
690+
691+ // When - Perform multiple concurrent operations that trigger KVO
692+ for i in 0 ..< 10 {
693+ DispatchQueue . global ( ) . async {
694+ let siteID = Int64 ( i)
695+ self . errorHandler. flagSiteAsUnsupported (
696+ for: siteID,
697+ flow: . apiRequest,
698+ cause: . majorError,
699+ error: NetworkError . notFound ( response: nil )
700+ )
701+ completionExpectation. fulfill ( )
702+ }
703+ }
704+
705+ // Then - Should complete without deadlock
706+ let result = XCTWaiter . wait ( for: [ completionExpectation] , timeout: 3.0 )
707+ XCTAssertEqual ( result, . completed, " Concurrent operations with multiple KVO observers should not deadlock " )
708+
709+ // Cleanup
710+ observations. forEach { $0. invalidate ( ) }
711+ }
609712}
610713
611714// MARK: - Helper Methods
0 commit comments