github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libkbfs/subscription_manager_test.go (about) 1 // Copyright 2019 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libkbfs 6 7 import ( 8 "fmt" 9 "reflect" 10 "testing" 11 "time" 12 13 gomock "github.com/golang/mock/gomock" 14 "github.com/keybase/client/go/kbfs/data" 15 "github.com/keybase/client/go/kbfs/favorites" 16 "github.com/keybase/client/go/kbfs/libcontext" 17 "github.com/keybase/client/go/kbfs/tlf" 18 keybase1 "github.com/keybase/client/go/protocol/keybase1" 19 "github.com/stretchr/testify/require" 20 "golang.org/x/net/context" 21 ) 22 23 func waitForCall(t *testing.T, timeout time.Duration) ( 24 waiter func(), done func(args ...interface{})) { 25 ch := make(chan struct{}) 26 return func() { 27 select { 28 case <-time.After(timeout): 29 t.Fatalf("waiting on lastMockDone timeout") 30 case <-ch: 31 } 32 }, func(args ...interface{}) { 33 ch <- struct{}{} 34 } 35 } 36 37 const testSubscriptionManagerClientID SubscriptionManagerClientID = "test" 38 39 func initSubscriptionManagerTest(t *testing.T) (config Config, 40 sm SubscriptionManager, notifier *MockSubscriptionNotifier, 41 finish func()) { 42 ctl := gomock.NewController(t) 43 config = MakeTestConfigOrBust(t, "jdoe") 44 notifier = NewMockSubscriptionNotifier(ctl) 45 sm = config.SubscriptionManager( 46 testSubscriptionManagerClientID, false, notifier) 47 return config, sm, notifier, func() { 48 err := config.Shutdown(context.Background()) 49 require.NoError(t, err) 50 ctl.Finish() 51 } 52 } 53 54 type sliceMatcherNoOrder struct { 55 x interface{} 56 } 57 58 func (e sliceMatcherNoOrder) Matches(x interface{}) bool { 59 vExpected := reflect.ValueOf(e.x) 60 vGot := reflect.ValueOf(x) 61 if vExpected.Kind() != reflect.Slice || vGot.Kind() != reflect.Slice { 62 return false 63 } 64 if vExpected.Len() != vGot.Len() { 65 return false 66 } 67 outer: // O(n^2) (to avoid more complicated reflect) but it's usually small. 68 for i := 0; i < vExpected.Len(); i++ { 69 for j := 0; j < vGot.Len(); j++ { 70 if reflect.DeepEqual(vExpected.Index(i).Interface(), vGot.Index(j).Interface()) { 71 continue outer 72 } 73 } 74 return false 75 } 76 return true 77 } 78 79 func (e sliceMatcherNoOrder) String() string { 80 return fmt.Sprintf("is %v (but order doesn't matter)", e.x) 81 } 82 83 func TestSubscriptionManagerSubscribePath(t *testing.T) { 84 config, sm, notifier, finish := initSubscriptionManagerTest(t) 85 defer finish() 86 87 ctx, cancelFn := context.WithCancel(context.Background()) 88 defer cancelFn() 89 ctx, err := libcontext.NewContextWithCancellationDelayer( 90 libcontext.NewContextReplayable( 91 ctx, func(c context.Context) context.Context { 92 return ctx 93 })) 94 require.NoError(t, err) 95 96 waiter0, done0 := waitForCall(t, 4*time.Second) 97 waiter1, done1 := waitForCall(t, 4*time.Second) 98 waiter2, done2 := waitForCall(t, 4*time.Second) 99 waiter3, done3 := waitForCall(t, 4*time.Second) 100 101 tlfHandle, err := GetHandleFromFolderNameAndType( 102 ctx, config.KBPKI(), config.MDOps(), config, "jdoe", tlf.Private) 103 require.NoError(t, err) 104 rootNode, _, err := config.KBFSOps().GetOrCreateRootNode( 105 ctx, tlfHandle, data.MasterBranch) 106 require.NoError(t, err) 107 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 108 require.NoError(t, err) 109 110 sid1, sid2 := SubscriptionID("sid1"), SubscriptionID("sid2") 111 112 t.Logf("Subscribe to CHILDREN at TLF root using sid1, and create a file. We should get a notification.") 113 err = sm.SubscribePath(ctx, sid1, "/keybase/private/jdoe", 114 keybase1.PathSubscriptionTopic_CHILDREN, nil) 115 require.NoError(t, err) 116 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 117 []SubscriptionID{sid1}, "/keybase/private/jdoe", 118 []keybase1.PathSubscriptionTopic{keybase1.PathSubscriptionTopic_CHILDREN}) 119 fileNode, _, err := config.KBFSOps().CreateFile(ctx, rootNode, rootNode.ChildName("file"), false, NoExcl) 120 require.NoError(t, err) 121 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 122 require.NoError(t, err) 123 124 t.Logf("Try to subscribe using sid1 again, and it should fail") 125 err = sm.SubscribePath(ctx, sid1, "/keybase/private/jdoe", 126 keybase1.PathSubscriptionTopic_STAT, nil) 127 require.Error(t, err) 128 129 t.Logf("Subscribe to STAT at TLF root using sid2, and create a dir. We should get a notification for STAT, and a notificiation for CHILDREN.") 130 err = sm.SubscribePath(ctx, sid2, "/keybase/private/jdoe", 131 keybase1.PathSubscriptionTopic_STAT, nil) 132 require.NoError(t, err) 133 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 134 sliceMatcherNoOrder{[]SubscriptionID{sid1, sid2}}, 135 "/keybase/private/jdoe", 136 sliceMatcherNoOrder{[]keybase1.PathSubscriptionTopic{ 137 keybase1.PathSubscriptionTopic_STAT, 138 keybase1.PathSubscriptionTopic_CHILDREN, 139 }}).Do(func( 140 clientID SubscriptionManagerClientID, subscriptionIDs []SubscriptionID, 141 path string, topics []keybase1.PathSubscriptionTopic) { 142 done0() 143 done1() 144 }) 145 _, _, err = config.KBFSOps().CreateDir( 146 ctx, rootNode, rootNode.ChildName("dir1")) 147 require.NoError(t, err) 148 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 149 require.NoError(t, err) 150 151 // These waits are needed to avoid races. 152 t.Logf("Waiting for last notifications (done0 and done1) before unsubscribing.") 153 waiter0() 154 waiter1() 155 156 t.Logf("Unsubscribe sid1, and make another dir. We should only get a notification for STAT.") 157 sm.Unsubscribe(ctx, sid1) 158 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 159 []SubscriptionID{sid2}, "/keybase/private/jdoe", 160 []keybase1.PathSubscriptionTopic{keybase1.PathSubscriptionTopic_STAT}).Do(func( 161 clientID SubscriptionManagerClientID, subscriptionIDs []SubscriptionID, 162 path string, topics []keybase1.PathSubscriptionTopic) { 163 done2() 164 }) 165 166 _, _, err = config.KBFSOps().CreateDir( 167 ctx, rootNode, rootNode.ChildName("dir2")) 168 require.NoError(t, err) 169 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 170 require.NoError(t, err) 171 172 t.Logf("Waiting for last notification (done2) before unsubscribing.") 173 waiter2() 174 175 t.Logf("Unsubscribe sid2 as well. Then subscribe to STAT on the file using sid1 (which we unsubscribed earlier), and write to it. We should get STAT notification.") 176 sm.Unsubscribe(ctx, sid2) 177 err = sm.SubscribePath(ctx, sid1, "/keybase/private/jdoe/dir1/../file", keybase1.PathSubscriptionTopic_STAT, nil) 178 require.NoError(t, err) 179 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 180 []SubscriptionID{sid1}, "/keybase/private/jdoe/dir1/../file", 181 []keybase1.PathSubscriptionTopic{keybase1.PathSubscriptionTopic_STAT}).Do(func( 182 clientID SubscriptionManagerClientID, subscriptionIDs []SubscriptionID, 183 path string, topics []keybase1.PathSubscriptionTopic) { 184 done3() 185 }) 186 err = config.KBFSOps().Write(ctx, fileNode, []byte("hello"), 0) 187 require.NoError(t, err) 188 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 189 require.NoError(t, err) 190 191 t.Logf("Waiting for last notification (done3) before finishing the test.") 192 waiter3() 193 } 194 195 func TestSubscriptionManagerFavoritesChange(t *testing.T) { 196 config, sm, notifier, finish := initSubscriptionManagerTest(t) 197 defer finish() 198 ctx := context.Background() 199 200 waiter1, done1 := waitForCall(t, 4*time.Second) 201 202 sid1 := SubscriptionID("sid1") 203 err := sm.SubscribeNonPath(ctx, sid1, keybase1.SubscriptionTopic_FAVORITES, nil) 204 require.NoError(t, err) 205 notifier.EXPECT().OnNonPathChange( 206 testSubscriptionManagerClientID, 207 []SubscriptionID{sid1}, keybase1.SubscriptionTopic_FAVORITES).Do( 208 func( 209 clientID SubscriptionManagerClientID, subscriptionIDs []SubscriptionID, 210 topic keybase1.SubscriptionTopic) { 211 done1() 212 }) 213 err = config.KBFSOps().AddFavorite(ctx, 214 favorites.Folder{ 215 Name: "test", 216 Type: tlf.Public, 217 }, 218 favorites.Data{}, 219 ) 220 require.NoError(t, err) 221 222 t.Logf("Waiting for last notification (done1) before finishing the test.") 223 waiter1() 224 } 225 226 func TestSubscriptionManagerSubscribePathNoFolderBranch(t *testing.T) { 227 config, sm, notifier, finish := initSubscriptionManagerTest(t) 228 defer finish() 229 230 ctx, cancelFn := context.WithCancel(context.Background()) 231 defer cancelFn() 232 ctx, err := libcontext.NewContextWithCancellationDelayer( 233 libcontext.NewContextReplayable( 234 ctx, func(c context.Context) context.Context { 235 return ctx 236 })) 237 require.NoError(t, err) 238 239 waiter0, done0 := waitForCall(t, 4*time.Second) 240 241 t.Logf("Subscribe to CHILDREN at TLF root using sid1, before we have a folderBranch. Then create a file. We should get a notification.") 242 sid1 := SubscriptionID("sid1") 243 244 err = sm.SubscribePath(ctx, sid1, "/keybase/private/jdoe", 245 keybase1.PathSubscriptionTopic_CHILDREN, nil) 246 require.NoError(t, err) 247 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 248 []SubscriptionID{sid1}, "/keybase/private/jdoe", 249 []keybase1.PathSubscriptionTopic{keybase1.PathSubscriptionTopic_CHILDREN}).AnyTimes().Do( 250 func( 251 clientID SubscriptionManagerClientID, subscriptionIDs []SubscriptionID, 252 path string, topics []keybase1.PathSubscriptionTopic) { 253 done0() 254 }) 255 256 tlfHandle, err := GetHandleFromFolderNameAndType( 257 ctx, config.KBPKI(), config.MDOps(), config, "jdoe", tlf.Private) 258 require.NoError(t, err) 259 rootNode, _, err := config.KBFSOps().GetOrCreateRootNode( 260 ctx, tlfHandle, data.MasterBranch) 261 require.NoError(t, err) 262 _, _, err = config.KBFSOps().CreateFile( 263 ctx, rootNode, rootNode.ChildName("file"), false, NoExcl) 264 require.NoError(t, err) 265 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 266 require.NoError(t, err) 267 268 waiter0() 269 }