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  }