1// Copyright (C) 2016 The Syncthing Authors.
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this file,
5// You can obtain one at https://mozilla.org/MPL/2.0/.
6
7package model
8
9import (
10	"context"
11	"io/ioutil"
12	"os"
13	"testing"
14	"time"
15
16	"github.com/syncthing/syncthing/lib/config"
17	"github.com/syncthing/syncthing/lib/db"
18	"github.com/syncthing/syncthing/lib/db/backend"
19	"github.com/syncthing/syncthing/lib/events"
20	"github.com/syncthing/syncthing/lib/fs"
21	"github.com/syncthing/syncthing/lib/ignore"
22	"github.com/syncthing/syncthing/lib/protocol"
23	"github.com/syncthing/syncthing/lib/rand"
24)
25
26var (
27	myID, device1, device2  protocol.DeviceID
28	defaultCfgWrapper       config.Wrapper
29	defaultCfgWrapperCancel context.CancelFunc
30	defaultFolderConfig     config.FolderConfiguration
31	defaultFs               fs.Filesystem
32	defaultCfg              config.Configuration
33	defaultAutoAcceptCfg    config.Configuration
34)
35
36func init() {
37	myID, _ = protocol.DeviceIDFromString("ZNWFSWE-RWRV2BD-45BLMCV-LTDE2UR-4LJDW6J-R5BPWEB-TXD27XJ-IZF5RA4")
38	device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
39	device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
40
41	defaultCfgWrapper, defaultCfgWrapperCancel = createTmpWrapper(config.New(myID))
42
43	defaultFolderConfig = testFolderConfig("testdata")
44	defaultFs = defaultFolderConfig.Filesystem()
45
46	waiter, _ := defaultCfgWrapper.Modify(func(cfg *config.Configuration) {
47		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device1, "device1"))
48		cfg.SetFolder(defaultFolderConfig)
49		cfg.Options.KeepTemporariesH = 1
50	})
51	waiter.Wait()
52
53	defaultCfg = defaultCfgWrapper.RawCopy()
54
55	defaultAutoAcceptCfg = config.Configuration{
56		Version: config.CurrentVersion,
57		Devices: []config.DeviceConfiguration{
58			{
59				DeviceID: myID, // self
60			},
61			{
62				DeviceID:          device1,
63				AutoAcceptFolders: true,
64			},
65			{
66				DeviceID:          device2,
67				AutoAcceptFolders: true,
68			},
69		},
70		Defaults: config.Defaults{
71			Folder: config.FolderConfiguration{
72				Path: ".",
73			},
74		},
75	}
76}
77
78func createTmpWrapper(cfg config.Configuration) (config.Wrapper, context.CancelFunc) {
79	tmpFile, err := ioutil.TempFile("", "syncthing-testConfig-")
80	if err != nil {
81		panic(err)
82	}
83	wrapper := config.Wrap(tmpFile.Name(), cfg, myID, events.NoopLogger)
84	tmpFile.Close()
85	ctx, cancel := context.WithCancel(context.Background())
86	go wrapper.Serve(ctx)
87	return wrapper, cancel
88}
89
90func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration, context.CancelFunc) {
91	w, cancel := createTmpWrapper(defaultCfgWrapper.RawCopy())
92	fcfg := testFolderConfigTmp()
93	_, _ = w.Modify(func(cfg *config.Configuration) {
94		cfg.SetFolder(fcfg)
95	})
96	return w, fcfg, cancel
97}
98
99func testFolderConfigTmp() config.FolderConfiguration {
100	tmpDir := createTmpDir()
101	return testFolderConfig(tmpDir)
102}
103
104func testFolderConfig(path string) config.FolderConfiguration {
105	cfg := newFolderConfiguration(defaultCfgWrapper, "default", "default", fs.FilesystemTypeBasic, path)
106	cfg.FSWatcherEnabled = false
107	cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{DeviceID: device1})
108	return cfg
109}
110
111func testFolderConfigFake() config.FolderConfiguration {
112	cfg := newFolderConfiguration(defaultCfgWrapper, "default", "default", fs.FilesystemTypeFake, rand.String(32)+"?content=true")
113	cfg.FSWatcherEnabled = false
114	cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{DeviceID: device1})
115	return cfg
116}
117
118func setupModelWithConnection(t testing.TB) (*testModel, *fakeConnection, config.FolderConfiguration, context.CancelFunc) {
119	t.Helper()
120	w, fcfg, cancel := tmpDefaultWrapper()
121	m, fc := setupModelWithConnectionFromWrapper(t, w)
122	return m, fc, fcfg, cancel
123}
124
125func setupModelWithConnectionFromWrapper(t testing.TB, w config.Wrapper) (*testModel, *fakeConnection) {
126	t.Helper()
127	m := setupModel(t, w)
128
129	fc := addFakeConn(m, device1, "default")
130	fc.folder = "default"
131
132	_ = m.ScanFolder("default")
133
134	return m, fc
135}
136
137func setupModel(t testing.TB, w config.Wrapper) *testModel {
138	t.Helper()
139	m := newModel(t, w, myID, "syncthing", "dev", nil)
140	m.ServeBackground()
141	<-m.started
142
143	m.ScanFolders()
144
145	return m
146}
147
148type testModel struct {
149	*model
150	t        testing.TB
151	cancel   context.CancelFunc
152	evCancel context.CancelFunc
153	stopped  chan struct{}
154}
155
156func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, protectedFiles []string) *testModel {
157	t.Helper()
158	evLogger := events.NewLogger()
159	ldb, err := db.NewLowlevel(backend.OpenMemory(), evLogger)
160	if err != nil {
161		t.Fatal(err)
162	}
163	m := NewModel(cfg, id, clientName, clientVersion, ldb, protectedFiles, evLogger).(*model)
164	ctx, cancel := context.WithCancel(context.Background())
165	go evLogger.Serve(ctx)
166	return &testModel{
167		model:    m,
168		evCancel: cancel,
169		stopped:  make(chan struct{}),
170		t:        t,
171	}
172}
173
174func (m *testModel) ServeBackground() {
175	ctx, cancel := context.WithCancel(context.Background())
176	m.cancel = cancel
177	go func() {
178		m.model.Serve(ctx)
179		close(m.stopped)
180	}()
181	<-m.started
182}
183
184func (m *testModel) testAvailability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
185	av, err := m.model.Availability(folder, file, block)
186	must(m.t, err)
187	return av
188}
189
190func (m *testModel) testCurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
191	f, ok, err := m.model.CurrentFolderFile(folder, file)
192	must(m.t, err)
193	return f, ok
194}
195
196func (m *testModel) testCompletion(device protocol.DeviceID, folder string) FolderCompletion {
197	comp, err := m.Completion(device, folder)
198	must(m.t, err)
199	return comp
200}
201
202func cleanupModel(m *testModel) {
203	if m.cancel != nil {
204		m.cancel()
205		<-m.stopped
206	}
207	m.evCancel()
208	m.db.Close()
209	os.Remove(m.cfg.ConfigPath())
210}
211
212func cleanupModelAndRemoveDir(m *testModel, dir string) {
213	cleanupModel(m)
214	os.RemoveAll(dir)
215}
216
217func createTmpDir() string {
218	tmpDir, err := ioutil.TempDir("", "syncthing_testFolder-")
219	if err != nil {
220		panic("Failed to create temporary testing dir")
221	}
222	return tmpDir
223}
224
225type alwaysChangedKey struct {
226	fs   fs.Filesystem
227	name string
228}
229
230// alwaysChanges is an ignore.ChangeDetector that always returns true on Changed()
231type alwaysChanged struct {
232	seen map[alwaysChangedKey]struct{}
233}
234
235func newAlwaysChanged() *alwaysChanged {
236	return &alwaysChanged{
237		seen: make(map[alwaysChangedKey]struct{}),
238	}
239}
240
241func (c *alwaysChanged) Remember(fs fs.Filesystem, name string, _ time.Time) {
242	c.seen[alwaysChangedKey{fs, name}] = struct{}{}
243}
244
245func (c *alwaysChanged) Reset() {
246	c.seen = make(map[alwaysChangedKey]struct{})
247}
248
249func (c *alwaysChanged) Seen(fs fs.Filesystem, name string) bool {
250	_, ok := c.seen[alwaysChangedKey{fs, name}]
251	return ok
252}
253
254func (c *alwaysChanged) Changed() bool {
255	return true
256}
257
258func localSize(t *testing.T, m Model, folder string) db.Counts {
259	t.Helper()
260	snap := dbSnapshot(t, m, folder)
261	defer snap.Release()
262	return snap.LocalSize()
263}
264
265func globalSize(t *testing.T, m Model, folder string) db.Counts {
266	t.Helper()
267	snap := dbSnapshot(t, m, folder)
268	defer snap.Release()
269	return snap.GlobalSize()
270}
271
272func receiveOnlyChangedSize(t *testing.T, m Model, folder string) db.Counts {
273	t.Helper()
274	snap := dbSnapshot(t, m, folder)
275	defer snap.Release()
276	return snap.ReceiveOnlyChangedSize()
277}
278
279func needSizeLocal(t *testing.T, m Model, folder string) db.Counts {
280	t.Helper()
281	snap := dbSnapshot(t, m, folder)
282	defer snap.Release()
283	return snap.NeedSize(protocol.LocalDeviceID)
284}
285
286func dbSnapshot(t *testing.T, m Model, folder string) *db.Snapshot {
287	t.Helper()
288	snap, err := m.DBSnapshot(folder)
289	if err != nil {
290		t.Fatal(err)
291	}
292	return snap
293}
294
295func fsetSnapshot(t *testing.T, fset *db.FileSet) *db.Snapshot {
296	t.Helper()
297	snap, err := fset.Snapshot()
298	if err != nil {
299		t.Fatal(err)
300	}
301	return snap
302}
303
304// Reach in and update the ignore matcher to one that always does
305// reloads when asked to, instead of checking file mtimes. This is
306// because we will be changing the files on disk often enough that the
307// mtimes will be unreliable to determine change status.
308func folderIgnoresAlwaysReload(t testing.TB, m *testModel, fcfg config.FolderConfiguration) {
309	t.Helper()
310	m.removeFolder(fcfg)
311	fset := newFileSet(t, fcfg.ID, fcfg.Filesystem(), m.db)
312	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
313	m.fmut.Lock()
314	m.addAndStartFolderLockedWithIgnores(fcfg, fset, ignores)
315	m.fmut.Unlock()
316}
317
318func basicClusterConfig(local, remote protocol.DeviceID, folders ...string) protocol.ClusterConfig {
319	var cc protocol.ClusterConfig
320	for _, folder := range folders {
321		cc.Folders = append(cc.Folders, protocol.Folder{
322			ID: folder,
323			Devices: []protocol.Device{
324				{
325					ID: local,
326				},
327				{
328					ID: remote,
329				},
330			},
331		})
332	}
333	return cc
334}
335
336func localIndexUpdate(m *testModel, folder string, fs []protocol.FileInfo) {
337	m.fmut.RLock()
338	fset := m.folderFiles[folder]
339	m.fmut.RUnlock()
340
341	fset.Update(protocol.LocalDeviceID, fs)
342	seq := fset.Sequence(protocol.LocalDeviceID)
343	filenames := make([]string, len(fs))
344	for i, file := range fs {
345		filenames[i] = file.Name
346	}
347	m.evLogger.Log(events.LocalIndexUpdated, map[string]interface{}{
348		"folder":    folder,
349		"items":     len(fs),
350		"filenames": filenames,
351		"sequence":  seq,
352		"version":   seq, // legacy for sequence
353	})
354}
355
356func newDeviceConfiguration(defaultCfg config.DeviceConfiguration, id protocol.DeviceID, name string) config.DeviceConfiguration {
357	cfg := defaultCfg.Copy()
358	cfg.DeviceID = id
359	cfg.Name = name
360	return cfg
361}
362
363func newFileSet(t testing.TB, folder string, fs fs.Filesystem, ldb *db.Lowlevel) *db.FileSet {
364	t.Helper()
365	fset, err := db.NewFileSet(folder, fs, ldb)
366	if err != nil {
367		t.Fatal(err)
368	}
369	return fset
370}
371
372func replace(t testing.TB, w config.Wrapper, to config.Configuration) {
373	t.Helper()
374	waiter, err := w.Modify(func(cfg *config.Configuration) {
375		*cfg = to
376	})
377	if err != nil {
378		t.Fatal(err)
379	}
380	waiter.Wait()
381}
382
383func pauseFolder(t testing.TB, w config.Wrapper, id string, paused bool) {
384	t.Helper()
385	waiter, err := w.Modify(func(cfg *config.Configuration) {
386		_, i, _ := cfg.Folder(id)
387		cfg.Folders[i].Paused = paused
388	})
389	if err != nil {
390		t.Fatal(err)
391	}
392	waiter.Wait()
393}
394
395func setFolder(t testing.TB, w config.Wrapper, fcfg config.FolderConfiguration) {
396	t.Helper()
397	waiter, err := w.Modify(func(cfg *config.Configuration) {
398		cfg.SetFolder(fcfg)
399	})
400	if err != nil {
401		t.Fatal(err)
402	}
403	waiter.Wait()
404}
405
406func pauseDevice(t testing.TB, w config.Wrapper, id protocol.DeviceID, paused bool) {
407	t.Helper()
408	waiter, err := w.Modify(func(cfg *config.Configuration) {
409		_, i, _ := cfg.Device(id)
410		cfg.Devices[i].Paused = paused
411	})
412	if err != nil {
413		t.Fatal(err)
414	}
415	waiter.Wait()
416}
417
418func setDevice(t testing.TB, w config.Wrapper, device config.DeviceConfiguration) {
419	t.Helper()
420	waiter, err := w.Modify(func(cfg *config.Configuration) {
421		cfg.SetDevice(device)
422	})
423	if err != nil {
424		t.Fatal(err)
425	}
426	waiter.Wait()
427}
428
429func addDevice2(t testing.TB, w config.Wrapper, fcfg config.FolderConfiguration) {
430	waiter, err := w.Modify(func(cfg *config.Configuration) {
431		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device2, "device2"))
432		fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2})
433		cfg.SetFolder(fcfg)
434	})
435	must(t, err)
436	waiter.Wait()
437}
438