@@ -404,6 +404,87 @@ func TestResetRestoreTokenConcurrentDisconnectReportsCancellationAndOverallState
404404 }
405405}
406406
407+ func TestCanceledResetReportsRollbackPersistenceFailures (t * testing.T ) {
408+ for _ , test := range []struct {
409+ name string
410+ lookupErrAt int
411+ saveErrAt int
412+ }{
413+ {name : "rollback lookup" , lookupErrAt : 2 },
414+ {name : "rollback save" , saveErrAt : 2 },
415+ } {
416+ t .Run (test .name , func (t * testing.T ) {
417+ backend := & controlledCredentialBackend {
418+ credentials : & airplay.SavedCredentials {RestoreToken : "restore-1" },
419+ lookupErrAt : test .lookupErrAt ,
420+ saveErrAt : test .saveErrAt ,
421+ }
422+ d , entry , _ , _ := newResetTestDaemon (t , backend )
423+ cleanupStarted := make (chan struct {})
424+ cleanupRelease := make (chan struct {})
425+ originalCancel := entry .cancelFn
426+ entry .cancelFn = func () {
427+ close (cleanupStarted )
428+ <- cleanupRelease
429+ originalCancel ()
430+ }
431+ defer d .Shutdown ()
432+
433+ resetResponse := make (chan Response , 1 )
434+ go func () {
435+ resetResponse <- d .handleResetRestoreToken (Request {Cmd : "reset-restore-token" , Target : resetTestTarget })
436+ }()
437+ waitForResetSignal (t , cleanupStarted , "cleanup did not start" )
438+ if response := d .handleDisconnect (Request {Cmd : "disconnect" , Target : resetTestTarget }); ! response .OK {
439+ t .Fatalf ("disconnect response = %+v" , response )
440+ }
441+ close (cleanupRelease )
442+
443+ response := waitForResetResponse (t , resetResponse )
444+ if response .OK || ! strings .Contains (response .Error , "rollback failed" ) {
445+ t .Fatalf ("reset response = %+v, want explicit rollback failure" , response )
446+ }
447+ })
448+ }
449+ }
450+
451+ func TestRestoreTokenResetReservationMigratesChangedCanvasExclusively (t * testing.T ) {
452+ d , entry , reservation , _ := newResetTestDaemon (t , & controlledCredentialBackend {})
453+ oldKey := reservation .key
454+ newKey := normalizedVideoCaptureKey (1280 , 720 , airplay .VideoCodecH264 )
455+ reservation .broadcast = nil
456+ reservation .resetReservedBy = entry
457+
458+ d .mu .Lock ()
459+ migrated , err := d .migrateRestoreTokenResetReservationLocked (entry , newKey )
460+ oldReleased := d .captureGroups [oldKey ] == nil
461+ newOwned := d .captureGroups [newKey ] == reservation
462+ d .mu .Unlock ()
463+ if err != nil || migrated != reservation || ! oldReleased || ! newOwned {
464+ t .Fatalf ("migration = %p, %v, oldReleased=%t newOwned=%t" , migrated , err , oldReleased , newOwned )
465+ }
466+
467+ occupiedKey := normalizedVideoCaptureKey (3840 , 2160 , airplay .VideoCodecH264 )
468+ occupied := & videoCaptureGroup {key : occupiedKey }
469+ d .mu .Lock ()
470+ d .captureGroups [occupiedKey ] = occupied
471+ _ , err = d .migrateRestoreTokenResetReservationLocked (entry , occupiedKey )
472+ stillOwned := d .captureGroups [newKey ] == reservation && entry .captureGroup == reservation
473+ d .mu .Unlock ()
474+ if err == nil || ! errors .Is (err , errCaptureGroupResetReserved ) || ! stillOwned {
475+ t .Fatalf ("occupied migration = %v, stillOwned=%t" , err , stillOwned )
476+ }
477+
478+ d .mu .Lock ()
479+ cleanup := d .detachStreamLocked (resetTestTarget )
480+ reservationReleased := d .captureGroups [newKey ] == nil
481+ d .mu .Unlock ()
482+ cleanup .run ()
483+ if ! reservationReleased {
484+ t .Fatal ("disconnect left migrated reservation behind" )
485+ }
486+ }
487+
407488func TestResetRestoreTokenClearsExclusiveTargetAndReconnectsActualPort (t * testing.T ) {
408489 listener , err := net .Listen ("tcp" , "127.0.0.1:0" )
409490 if err != nil {
@@ -499,9 +580,17 @@ type controlledCredentialBackend struct {
499580 saveStarted chan struct {}
500581 saveRelease <- chan struct {}
501582 saveOnce sync.Once
583+ lookupCalls int
584+ saveCalls int
585+ lookupErrAt int
586+ saveErrAt int
502587}
503588
504589func (b * controlledCredentialBackend ) Lookup (string ) (* airplay.SavedCredentials , error ) {
590+ b .lookupCalls ++
591+ if b .lookupErrAt > 0 && b .lookupCalls == b .lookupErrAt {
592+ return nil , errors .New ("injected lookup failure" )
593+ }
505594 if b .lookupErr != nil {
506595 err := b .lookupErr
507596 b .lookupErr = nil
@@ -521,6 +610,10 @@ func (b *controlledCredentialBackend) Save(_ string, credentials *airplay.SavedC
521610 if b .saveRelease != nil {
522611 <- b .saveRelease
523612 }
613+ b .saveCalls ++
614+ if b .saveErrAt > 0 && b .saveCalls == b .saveErrAt {
615+ return errors .New ("injected save failure" )
616+ }
524617 if b .saveErr != nil {
525618 return b .saveErr
526619 }
0 commit comments