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