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 5package libkbfs 6 7import ( 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 23func 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 37const testSubscriptionManagerClientID SubscriptionManagerClientID = "test" 38 39func 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 54type sliceMatcherNoOrder struct { 55 x interface{} 56} 57 58func (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 } 67outer: // 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 79func (e sliceMatcherNoOrder) String() string { 80 return fmt.Sprintf("is %v (but order doesn't matter)", e.x) 81} 82 83func 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(args ...interface{}) { done0(args...); done1(args...) }) 140 _, _, err = config.KBFSOps().CreateDir( 141 ctx, rootNode, rootNode.ChildName("dir1")) 142 require.NoError(t, err) 143 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 144 require.NoError(t, err) 145 146 // These waits are needed to avoid races. 147 t.Logf("Waiting for last notifications (done0 and done1) before unsubscribing.") 148 waiter0() 149 waiter1() 150 151 t.Logf("Unsubscribe sid1, and make another dir. We should only get a notification for STAT.") 152 sm.Unsubscribe(ctx, sid1) 153 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 154 []SubscriptionID{sid2}, "/keybase/private/jdoe", 155 []keybase1.PathSubscriptionTopic{keybase1.PathSubscriptionTopic_STAT}).Do(done2) 156 _, _, err = config.KBFSOps().CreateDir( 157 ctx, rootNode, rootNode.ChildName("dir2")) 158 require.NoError(t, err) 159 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 160 require.NoError(t, err) 161 162 t.Logf("Waiting for last notification (done2) before unsubscribing.") 163 waiter2() 164 165 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.") 166 sm.Unsubscribe(ctx, sid2) 167 err = sm.SubscribePath(ctx, sid1, "/keybase/private/jdoe/dir1/../file", keybase1.PathSubscriptionTopic_STAT, nil) 168 require.NoError(t, err) 169 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 170 []SubscriptionID{sid1}, "/keybase/private/jdoe/dir1/../file", 171 []keybase1.PathSubscriptionTopic{keybase1.PathSubscriptionTopic_STAT}).Do(done3) 172 err = config.KBFSOps().Write(ctx, fileNode, []byte("hello"), 0) 173 require.NoError(t, err) 174 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 175 require.NoError(t, err) 176 177 t.Logf("Waiting for last notification (done3) before finishing the test.") 178 waiter3() 179} 180 181func TestSubscriptionManagerFavoritesChange(t *testing.T) { 182 config, sm, notifier, finish := initSubscriptionManagerTest(t) 183 defer finish() 184 ctx := context.Background() 185 186 waiter1, done1 := waitForCall(t, 4*time.Second) 187 188 sid1 := SubscriptionID("sid1") 189 err := sm.SubscribeNonPath(ctx, sid1, keybase1.SubscriptionTopic_FAVORITES, nil) 190 require.NoError(t, err) 191 notifier.EXPECT().OnNonPathChange( 192 testSubscriptionManagerClientID, 193 []SubscriptionID{sid1}, keybase1.SubscriptionTopic_FAVORITES).Do(done1) 194 err = config.KBFSOps().AddFavorite(ctx, 195 favorites.Folder{ 196 Name: "test", 197 Type: tlf.Public, 198 }, 199 favorites.Data{}, 200 ) 201 require.NoError(t, err) 202 203 t.Logf("Waiting for last notification (done1) before finishing the test.") 204 waiter1() 205} 206 207func TestSubscriptionManagerSubscribePathNoFolderBranch(t *testing.T) { 208 config, sm, notifier, finish := initSubscriptionManagerTest(t) 209 defer finish() 210 211 ctx, cancelFn := context.WithCancel(context.Background()) 212 defer cancelFn() 213 ctx, err := libcontext.NewContextWithCancellationDelayer( 214 libcontext.NewContextReplayable( 215 ctx, func(c context.Context) context.Context { 216 return ctx 217 })) 218 require.NoError(t, err) 219 220 waiter0, done0 := waitForCall(t, 4*time.Second) 221 222 t.Logf("Subscribe to CHILDREN at TLF root using sid1, before we have a folderBranch. Then create a file. We should get a notification.") 223 sid1 := SubscriptionID("sid1") 224 225 err = sm.SubscribePath(ctx, sid1, "/keybase/private/jdoe", 226 keybase1.PathSubscriptionTopic_CHILDREN, nil) 227 require.NoError(t, err) 228 notifier.EXPECT().OnPathChange(testSubscriptionManagerClientID, 229 []SubscriptionID{sid1}, "/keybase/private/jdoe", 230 []keybase1.PathSubscriptionTopic{keybase1.PathSubscriptionTopic_CHILDREN}).AnyTimes().Do(done0) 231 232 tlfHandle, err := GetHandleFromFolderNameAndType( 233 ctx, config.KBPKI(), config.MDOps(), config, "jdoe", tlf.Private) 234 require.NoError(t, err) 235 rootNode, _, err := config.KBFSOps().GetOrCreateRootNode( 236 ctx, tlfHandle, data.MasterBranch) 237 require.NoError(t, err) 238 _, _, err = config.KBFSOps().CreateFile( 239 ctx, rootNode, rootNode.ChildName("file"), false, NoExcl) 240 require.NoError(t, err) 241 err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 242 require.NoError(t, err) 243 244 waiter0() 245} 246