@@ -469,6 +469,236 @@ func TestUserKeysHash(t *testing.T) {
469469 }
470470}
471471
472+ func TestCompactCollectionChannelHistory (t * testing.T ) {
473+ base .SetUpTestLogging (t , base .LevelDebug , base .KeyAuth )
474+
475+ ctx := base .TestCtx (t )
476+ testBucket := base .GetTestBucket (t )
477+ defer testBucket .Close (ctx )
478+ dataStore := testBucket .GetSingleDataStore ()
479+
480+ const (
481+ scope = "scope1"
482+ collection = "collection1"
483+ )
484+
485+ // Helper to create fresh history state for each subtest
486+ newHistory := func () TimedSetHistory {
487+ return TimedSetHistory {
488+ "ch1" : GrantHistory {
489+ UpdatedAt : 1000 ,
490+ Entries : []GrantHistorySequencePair {{StartSeq : 1 , EndSeq : 10 }},
491+ },
492+ "ch2" : GrantHistory {
493+ UpdatedAt : 2000 ,
494+ Entries : []GrantHistorySequencePair {{StartSeq : 11 , EndSeq : 20 }},
495+ },
496+ "ch3" : GrantHistory {
497+ UpdatedAt : 3000 ,
498+ Entries : []GrantHistorySequencePair {{StartSeq : 21 , EndSeq : 30 }},
499+ },
500+ }
501+ }
502+
503+ t .Run ("CompactsExistingChannels" , func (t * testing.T ) {
504+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
505+ user , err := auth .NewUser ("user" , "password" , base.Set {})
506+ require .NoError (t , err )
507+ u := user .(* userImpl )
508+
509+ u .SetCollectionChannelHistory (scope , collection , newHistory ())
510+
511+ compacted := u .CompactChannelHistory (scope , collection , []string {"ch1" , "ch2" })
512+
513+ require .ElementsMatch (t , []string {"ch1" , "ch2" }, compacted )
514+
515+ afterHistory := u .CollectionChannelHistory (scope , collection )
516+ _ , ch1Present := afterHistory ["ch1" ]
517+ _ , ch2Present := afterHistory ["ch2" ]
518+ assert .False (t , ch1Present , "ch1 should have been removed from channel history" )
519+ assert .False (t , ch2Present , "ch2 should have been removed from channel history" )
520+
521+ ch3Entry , ch3Present := afterHistory ["ch3" ]
522+ require .True (t , ch3Present , "ch3 should remain in channel history" )
523+ assert .Equal (t , int64 (3000 ), ch3Entry .UpdatedAt )
524+ require .Len (t , ch3Entry .Entries , 1 )
525+ assert .Equal (t , GrantHistorySequencePair {StartSeq : 21 , EndSeq : 30 }, ch3Entry .Entries [0 ])
526+ })
527+
528+ t .Run ("NonExistentChannelReturnsEmpty" , func (t * testing.T ) {
529+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
530+ user , err := auth .NewUser ("user" , "password" , base.Set {})
531+ require .NoError (t , err )
532+ u := user .(* userImpl )
533+
534+ u .SetCollectionChannelHistory (scope , collection , newHistory ())
535+
536+ compacted := u .CompactChannelHistory (scope , collection , []string {"doesNotExist" })
537+ assert .Empty (t , compacted )
538+
539+ afterHistory := u .CollectionChannelHistory (scope , collection )
540+ require .Len (t , afterHistory , 3 , "all three channels should remain in history" )
541+ _ , ch1Present := afterHistory ["ch1" ]
542+ _ , ch2Present := afterHistory ["ch2" ]
543+ _ , ch3Present := afterHistory ["ch3" ]
544+ assert .True (t , ch1Present )
545+ assert .True (t , ch2Present )
546+ assert .True (t , ch3Present )
547+ })
548+
549+ t .Run ("MultipleCollectionsIsolated" , func (t * testing.T ) {
550+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
551+ user , err := auth .NewUser ("user" , "password" , base.Set {})
552+ require .NoError (t , err )
553+ u := user .(* userImpl )
554+
555+ const (
556+ scope2 = "scope2"
557+ collection2 = "collection2"
558+ )
559+
560+ // Set up history in both collections
561+ u .SetCollectionChannelHistory (scope , collection , newHistory ())
562+ u .SetCollectionChannelHistory (scope2 , collection2 , newHistory ())
563+
564+ // Compact channels in the first collection only
565+ compacted := u .CompactChannelHistory (scope , collection , []string {"ch1" })
566+ require .ElementsMatch (t , []string {"ch1" }, compacted )
567+
568+ // Verify first collection is modified
569+ history1 := u .CollectionChannelHistory (scope , collection )
570+ _ , ch1Present := history1 ["ch1" ]
571+ assert .False (t , ch1Present , "ch1 should be removed from first collection" )
572+ require .Len (t , history1 , 2 , "first collection should have 2 channels left" )
573+
574+ // Verify second collection is untouched
575+ history2 := u .CollectionChannelHistory (scope2 , collection2 )
576+ require .Len (t , history2 , 3 , "second collection should still have all 3 channels" )
577+ _ , ch1PresentInSecond := history2 ["ch1" ]
578+ assert .True (t , ch1PresentInSecond , "ch1 should still be in second collection" )
579+ })
580+
581+ t .Run ("NonExistentCollectionReturnsEmpty" , func (t * testing.T ) {
582+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
583+ user , err := auth .NewUser ("user" , "password" , base.Set {})
584+ require .NoError (t , err )
585+ u := user .(* userImpl )
586+
587+ const (
588+ nonExistentScope = "nonexistent"
589+ nonExistentCollection = "nothere"
590+ )
591+
592+ // Try to compact a scope/collection that was never set up
593+ compacted := u .CompactChannelHistory (nonExistentScope , nonExistentCollection , []string {"ch1" , "ch2" })
594+ assert .Empty (t , compacted , "compacting non-existent collection should return empty slice" )
595+
596+ // Verify the operation doesn't create an entry
597+ history := u .CollectionChannelHistory (nonExistentScope , nonExistentCollection )
598+ assert .Nil (t , history , "non-existent collection should return nil history" )
599+ })
600+
601+ t .Run ("NilHistoryForExistingCollection" , func (t * testing.T ) {
602+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
603+ user , err := auth .NewUser ("user" , "password" , base.Set {})
604+ require .NoError (t , err )
605+ u := user .(* userImpl )
606+
607+ // Grant explicit channels so the CollectionAccess entry exists, but no history is set.
608+ // This mirrors a user who has active channels but has never had any revoked.
609+ user .SetCollectionExplicitChannels (scope , collection , channels .AtSequence (channels .BaseSetOf (t , "ch1" ), 1 ), 0 )
610+
611+ // CollectionChannelHistory returns nil via cc.ChannelHistory_ (collection exists, history does not),
612+ // which is distinct from the NonExistentCollectionReturnsEmpty case where getCollectionAccess
613+ // returns false entirely.
614+ require .Nil (t , u .CollectionChannelHistory (scope , collection ))
615+
616+ // CompactChannelHistory must handle nil history gracefully and return empty.
617+ compacted := u .CompactChannelHistory (scope , collection , []string {"ch1" , "ch99" })
618+ assert .Empty (t , compacted )
619+
620+ // Explicit channels must be untouched by the compaction.
621+ cc , ok := u .getCollectionAccess (scope , collection )
622+ require .True (t , ok )
623+ assert .True (t , cc .ExplicitChannels_ .Contains ("ch1" ))
624+ })
625+
626+ t .Run ("MixOfExistingAndNonExistingChannels" , func (t * testing.T ) {
627+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
628+ user , err := auth .NewUser ("user" , "password" , base.Set {})
629+ require .NoError (t , err )
630+ u := user .(* userImpl )
631+
632+ u .SetCollectionChannelHistory (scope , collection , newHistory ())
633+
634+ // Compact a mix: ch1 exists, ch2 exists, ch99 doesn't, ch3 exists
635+ compacted := u .CompactChannelHistory (scope , collection , []string {"ch1" , "ch99" , "ch2" , "ch3" })
636+
637+ // Only the ones that existed should be returned
638+ require .ElementsMatch (t , []string {"ch1" , "ch2" , "ch3" }, compacted )
639+
640+ // All three should be removed
641+ afterHistory := u .CollectionChannelHistory (scope , collection )
642+ assert .Len (t , afterHistory , 0 , "all channels should have been removed" )
643+ })
644+
645+ t .Run ("DuplicateChannelNamesInInput" , func (t * testing.T ) {
646+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
647+ user , err := auth .NewUser ("user" , "password" , base.Set {})
648+ require .NoError (t , err )
649+ u := user .(* userImpl )
650+
651+ u .SetCollectionChannelHistory (scope , collection , newHistory ())
652+
653+ // Pass duplicate channel names: ch1 twice, ch2 once
654+ compacted := u .CompactChannelHistory (scope , collection , []string {"ch1" , "ch1" , "ch2" })
655+
656+ // First ch1 succeeds (returns true), second ch1 fails (already deleted, returns false in the map check)
657+ // So we get ch1 and ch2 in the result. The function returns duplicates if passed duplicates.
658+ require .ElementsMatch (t , []string {"ch1" , "ch2" }, compacted )
659+
660+ // Verify ch1 and ch2 are removed (only once, idempotently)
661+ afterHistory := u .CollectionChannelHistory (scope , collection )
662+ _ , ch1Present := afterHistory ["ch1" ]
663+ _ , ch2Present := afterHistory ["ch2" ]
664+ assert .False (t , ch1Present )
665+ assert .False (t , ch2Present )
666+
667+ // ch3 should remain
668+ _ , ch3Present := afterHistory ["ch3" ]
669+ require .True (t , ch3Present )
670+ require .Len (t , afterHistory , 1 )
671+ })
672+
673+ t .Run ("DefaultCollection" , func (t * testing.T ) {
674+ auth := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
675+ user , err := auth .NewUser ("user" , "password" , base.Set {})
676+ require .NoError (t , err )
677+ u := user .(* userImpl )
678+
679+ // Set up history for the default collection
680+ u .SetCollectionChannelHistory (base .DefaultScope , base .DefaultCollection , newHistory ())
681+
682+ // Verify history is accessible via default collection
683+ history := u .CollectionChannelHistory (base .DefaultScope , base .DefaultCollection )
684+ require .Len (t , history , 3 )
685+
686+ // Compact channels in default collection
687+ compacted := u .CompactChannelHistory (base .DefaultScope , base .DefaultCollection , []string {"ch1" , "ch3" })
688+ require .ElementsMatch (t , []string {"ch1" , "ch3" }, compacted )
689+
690+ // Verify removal
691+ afterHistory := u .CollectionChannelHistory (base .DefaultScope , base .DefaultCollection )
692+ _ , ch1Present := afterHistory ["ch1" ]
693+ _ , ch2Present := afterHistory ["ch2" ]
694+ _ , ch3Present := afterHistory ["ch3" ]
695+ assert .False (t , ch1Present )
696+ assert .True (t , ch2Present , "ch2 should remain" )
697+ assert .False (t , ch3Present )
698+ require .Len (t , afterHistory , 1 )
699+ })
700+ }
701+
472702func docExists (t * testing.T , dataStore base.DataStore , key string ) {
473703 _ , _ , err := dataStore .GetRaw (key )
474704 require .Nil (t , err , "doc %s should exist in datastore" , key )
0 commit comments