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	"bytes"
11	"context"
12	"errors"
13	"io/ioutil"
14	"os"
15	"path/filepath"
16	"runtime"
17	"strconv"
18	"strings"
19	"sync"
20	"testing"
21	"time"
22
23	"github.com/syncthing/syncthing/lib/config"
24	"github.com/syncthing/syncthing/lib/events"
25	"github.com/syncthing/syncthing/lib/fs"
26	"github.com/syncthing/syncthing/lib/protocol"
27	"github.com/syncthing/syncthing/lib/rand"
28)
29
30func TestRequestSimple(t *testing.T) {
31	// Verify that the model performs a request and creates a file based on
32	// an incoming index update.
33
34	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
35	defer wcfgCancel()
36	tfs := fcfg.Filesystem()
37	defer cleanupModelAndRemoveDir(m, tfs.URI())
38
39	// We listen for incoming index updates and trigger when we see one for
40	// the expected test file.
41	done := make(chan struct{})
42	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
43		select {
44		case <-done:
45			t.Error("More than one index update sent")
46		default:
47		}
48		for _, f := range fs {
49			if f.Name == "testfile" {
50				close(done)
51				return nil
52			}
53		}
54		return nil
55	})
56
57	// Send an update for the test file, wait for it to sync and be reported back.
58	contents := []byte("test file contents\n")
59	fc.addFile("testfile", 0644, protocol.FileInfoTypeFile, contents)
60	fc.sendIndexUpdate()
61	select {
62	case <-done:
63	case <-time.After(10 * time.Second):
64		t.Fatal("timed out")
65	}
66
67	// Verify the contents
68	if err := equalContents(filepath.Join(tfs.URI(), "testfile"), contents); err != nil {
69		t.Error("File did not sync correctly:", err)
70	}
71}
72
73func TestSymlinkTraversalRead(t *testing.T) {
74	// Verify that a symlink can not be traversed for reading.
75
76	if runtime.GOOS == "windows" {
77		t.Skip("no symlink support on CI")
78		return
79	}
80
81	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
82	defer wcfgCancel()
83	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
84
85	// We listen for incoming index updates and trigger when we see one for
86	// the expected test file.
87	done := make(chan struct{})
88	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
89		select {
90		case <-done:
91			t.Error("More than one index update sent")
92		default:
93		}
94		for _, f := range fs {
95			if f.Name == "symlink" {
96				close(done)
97				return nil
98			}
99		}
100		return nil
101	})
102
103	// Send an update for the symlink, wait for it to sync and be reported back.
104	contents := []byte("..")
105	fc.addFile("symlink", 0644, protocol.FileInfoTypeSymlink, contents)
106	fc.sendIndexUpdate()
107	<-done
108
109	// Request a file by traversing the symlink
110	res, err := m.Request(device1, "default", "symlink/requests_test.go", 0, 10, 0, nil, 0, false)
111	if err == nil || res != nil {
112		t.Error("Managed to traverse symlink")
113	}
114}
115
116func TestSymlinkTraversalWrite(t *testing.T) {
117	// Verify that a symlink can not be traversed for writing.
118
119	if runtime.GOOS == "windows" {
120		t.Skip("no symlink support on CI")
121		return
122	}
123
124	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
125	defer wcfgCancel()
126	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
127
128	// We listen for incoming index updates and trigger when we see one for
129	// the expected names.
130	done := make(chan struct{}, 1)
131	badReq := make(chan string, 1)
132	badIdx := make(chan string, 1)
133	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
134		for _, f := range fs {
135			if f.Name == "symlink" {
136				done <- struct{}{}
137				return nil
138			}
139			if strings.HasPrefix(f.Name, "symlink") {
140				badIdx <- f.Name
141				return nil
142			}
143		}
144		return nil
145	})
146	fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
147		if name != "symlink" && strings.HasPrefix(name, "symlink") {
148			badReq <- name
149		}
150		return fc.fileData[name], nil
151	})
152
153	// Send an update for the symlink, wait for it to sync and be reported back.
154	contents := []byte("..")
155	fc.addFile("symlink", 0644, protocol.FileInfoTypeSymlink, contents)
156	fc.sendIndexUpdate()
157	<-done
158
159	// Send an update for things behind the symlink, wait for requests for
160	// blocks for any of them to come back, or index entries. Hopefully none
161	// of that should happen.
162	contents = []byte("testdata testdata\n")
163	fc.addFile("symlink/testfile", 0644, protocol.FileInfoTypeFile, contents)
164	fc.addFile("symlink/testdir", 0644, protocol.FileInfoTypeDirectory, contents)
165	fc.addFile("symlink/testsyml", 0644, protocol.FileInfoTypeSymlink, contents)
166	fc.sendIndexUpdate()
167
168	select {
169	case name := <-badReq:
170		t.Fatal("Should not have requested the data for", name)
171	case name := <-badIdx:
172		t.Fatal("Should not have sent the index entry for", name)
173	case <-time.After(3 * time.Second):
174		// Unfortunately not much else to trigger on here. The puller sleep
175		// interval is 1s so if we didn't get any requests within two
176		// iterations we should be fine.
177	}
178}
179
180func TestRequestCreateTmpSymlink(t *testing.T) {
181	// Test that an update for a temporary file is invalidated
182
183	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
184	defer wcfgCancel()
185	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
186
187	// We listen for incoming index updates and trigger when we see one for
188	// the expected test file.
189	goodIdx := make(chan struct{})
190	name := fs.TempName("testlink")
191	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
192		for _, f := range fs {
193			if f.Name == name {
194				if f.IsInvalid() {
195					goodIdx <- struct{}{}
196				} else {
197					t.Error("Received index with non-invalid temporary file")
198					close(goodIdx)
199				}
200				return nil
201			}
202		}
203		return nil
204	})
205
206	// Send an update for the test file, wait for it to sync and be reported back.
207	fc.addFile(name, 0644, protocol.FileInfoTypeSymlink, []byte(".."))
208	fc.sendIndexUpdate()
209
210	select {
211	case <-goodIdx:
212	case <-time.After(3 * time.Second):
213		t.Fatal("Timed out without index entry being sent")
214	}
215}
216
217func TestRequestVersioningSymlinkAttack(t *testing.T) {
218	if runtime.GOOS == "windows" {
219		t.Skip("no symlink support on Windows")
220	}
221
222	// Sets up a folder with trashcan versioning and tries to use a
223	// deleted symlink to escape
224
225	w, fcfg, wCancel := tmpDefaultWrapper()
226	defer wCancel()
227	defer func() {
228		os.RemoveAll(fcfg.Filesystem().URI())
229		os.Remove(w.ConfigPath())
230	}()
231
232	fcfg.Versioning = config.VersioningConfiguration{Type: "trashcan"}
233	setFolder(t, w, fcfg)
234	m, fc := setupModelWithConnectionFromWrapper(t, w)
235	defer cleanupModel(m)
236
237	// Create a temporary directory that we will use as target to see if
238	// we can escape to it
239	tmpdir, err := ioutil.TempDir("", "syncthing-test")
240	if err != nil {
241		t.Fatal(err)
242	}
243	defer os.RemoveAll(tmpdir)
244
245	// We listen for incoming index updates and trigger when we see one for
246	// the expected test file.
247	idx := make(chan int)
248	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
249		idx <- len(fs)
250		return nil
251	})
252
253	waitForIdx := func() {
254		select {
255		case c := <-idx:
256			if c == 0 {
257				t.Fatal("Got empty index update")
258			}
259		case <-time.After(5 * time.Second):
260			t.Fatal("timed out before receiving index update")
261		}
262	}
263
264	// Send an update for the test file, wait for it to sync and be reported back.
265	fc.addFile("foo", 0644, protocol.FileInfoTypeSymlink, []byte(tmpdir))
266	fc.sendIndexUpdate()
267	waitForIdx()
268
269	// Delete the symlink, hoping for it to get versioned
270	fc.deleteFile("foo")
271	fc.sendIndexUpdate()
272	waitForIdx()
273
274	// Recreate foo and a file in it with some data
275	fc.updateFile("foo", 0755, protocol.FileInfoTypeDirectory, nil)
276	fc.addFile("foo/test", 0644, protocol.FileInfoTypeFile, []byte("testtesttest"))
277	fc.sendIndexUpdate()
278	waitForIdx()
279
280	// Remove the test file and see if it escaped
281	fc.deleteFile("foo/test")
282	fc.sendIndexUpdate()
283	waitForIdx()
284
285	path := filepath.Join(tmpdir, "test")
286	if _, err := os.Lstat(path); !os.IsNotExist(err) {
287		t.Fatal("File escaped to", path)
288	}
289}
290
291func TestPullInvalidIgnoredSO(t *testing.T) {
292	pullInvalidIgnored(t, config.FolderTypeSendOnly)
293
294}
295
296func TestPullInvalidIgnoredSR(t *testing.T) {
297	pullInvalidIgnored(t, config.FolderTypeSendReceive)
298}
299
300// This test checks that (un-)ignored/invalid/deleted files are treated as expected.
301func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
302	w, wCancel := createTmpWrapper(defaultCfgWrapper.RawCopy())
303	defer wCancel()
304	fcfg := testFolderConfigTmp()
305	fss := fcfg.Filesystem()
306	fcfg.Type = ft
307	setFolder(t, w, fcfg)
308	m := setupModel(t, w)
309	defer cleanupModelAndRemoveDir(m, fss.URI())
310
311	folderIgnoresAlwaysReload(t, m, fcfg)
312
313	fc := addFakeConn(m, device1, fcfg.ID)
314	fc.folder = "default"
315
316	if err := m.SetIgnores("default", []string{"*ignored*"}); err != nil {
317		panic(err)
318	}
319
320	contents := []byte("test file contents\n")
321	otherContents := []byte("other test file contents\n")
322
323	invIgn := "invalid:ignored"
324	invDel := "invalid:deleted"
325	ign := "ignoredNonExisting"
326	ignExisting := "ignoredExisting"
327
328	fc.addFile(invIgn, 0644, protocol.FileInfoTypeFile, contents)
329	fc.addFile(invDel, 0644, protocol.FileInfoTypeFile, contents)
330	fc.deleteFile(invDel)
331	fc.addFile(ign, 0644, protocol.FileInfoTypeFile, contents)
332	fc.addFile(ignExisting, 0644, protocol.FileInfoTypeFile, contents)
333	if err := writeFile(fss, ignExisting, otherContents, 0644); err != nil {
334		panic(err)
335	}
336
337	done := make(chan struct{})
338	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
339		expected := map[string]struct{}{invIgn: {}, ign: {}, ignExisting: {}}
340		for _, f := range fs {
341			if _, ok := expected[f.Name]; !ok {
342				t.Errorf("Unexpected file %v was added to index", f.Name)
343			}
344			if !f.IsInvalid() {
345				t.Errorf("File %v wasn't marked as invalid", f.Name)
346			}
347			delete(expected, f.Name)
348		}
349		for name := range expected {
350			t.Errorf("File %v wasn't added to index", name)
351		}
352		close(done)
353		return nil
354	})
355
356	sub := m.evLogger.Subscribe(events.FolderErrors)
357	defer sub.Unsubscribe()
358
359	fc.sendIndexUpdate()
360
361	select {
362	case ev := <-sub.C():
363		t.Fatalf("Errors while scanning/pulling: %v", ev)
364	case <-time.After(5 * time.Second):
365		t.Fatalf("timed out before index was received")
366	case <-done:
367	}
368
369	done = make(chan struct{})
370	expected := map[string]struct{}{ign: {}, ignExisting: {}}
371	var expectedMut sync.Mutex
372	// The indexes will normally arrive in one update, but it is possible
373	// that they arrive in separate ones.
374	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
375		expectedMut.Lock()
376		for _, f := range fs {
377			_, ok := expected[f.Name]
378			if !ok {
379				t.Errorf("Unexpected file %v was updated in index", f.Name)
380				continue
381			}
382			if f.IsInvalid() {
383				t.Errorf("File %v is still marked as invalid", f.Name)
384			}
385			if f.Name == ign {
386				// The unignored deleted file should have an
387				// empty version, to make it not override
388				// existing global files.
389				if !f.Deleted {
390					t.Errorf("File %v was not marked as deleted", f.Name)
391				}
392				if len(f.Version.Counters) != 0 {
393					t.Errorf("File %v has version %v, expected empty", f.Name, f.Version)
394				}
395			} else {
396				// The unignored existing file should have a
397				// version with only a local counter, to make
398				// it conflict changed global files.
399				if f.Deleted {
400					t.Errorf("File %v is marked as deleted", f.Name)
401				}
402				if len(f.Version.Counters) != 1 || f.Version.Counter(myID.Short()) == 0 {
403					t.Errorf("File %v has version %v, expected one entry for ourselves", f.Name, f.Version)
404				}
405			}
406			delete(expected, f.Name)
407		}
408		if len(expected) == 0 {
409			close(done)
410		}
411		expectedMut.Unlock()
412		return nil
413	})
414	// Make sure pulling doesn't interfere, as index updates are racy and
415	// thus we cannot distinguish between scan and pull results.
416	fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
417		return nil, nil
418	})
419
420	if err := m.SetIgnores("default", []string{"*:ignored*"}); err != nil {
421		panic(err)
422	}
423
424	select {
425	case <-time.After(5 * time.Second):
426		expectedMut.Lock()
427		t.Fatal("timed out before receiving index updates for all existing files, missing", expected)
428		expectedMut.Unlock()
429	case <-done:
430	}
431}
432
433func TestIssue4841(t *testing.T) {
434	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
435	defer wcfgCancel()
436	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
437
438	received := make(chan []protocol.FileInfo)
439	fc.setIndexFn(func(_ context.Context, _ string, fs []protocol.FileInfo) error {
440		received <- fs
441		return nil
442	})
443	checkReceived := func(fs []protocol.FileInfo) protocol.FileInfo {
444		t.Helper()
445		if len(fs) != 1 {
446			t.Fatalf("Sent index with %d files, should be 1", len(fs))
447		}
448		if fs[0].Name != "foo" {
449			t.Fatalf(`Sent index with file %v, should be "foo"`, fs[0].Name)
450		}
451		return fs[0]
452	}
453
454	// Setup file from remote that was ignored locally
455	folder := m.folderRunners[defaultFolderConfig.ID].(*sendReceiveFolder)
456	folder.updateLocals([]protocol.FileInfo{{
457		Name:       "foo",
458		Type:       protocol.FileInfoTypeFile,
459		LocalFlags: protocol.FlagLocalIgnored,
460		Version:    protocol.Vector{}.Update(device1.Short()),
461	}})
462
463	checkReceived(<-received)
464
465	// Scan without ignore patterns with "foo" not existing locally
466	if err := m.ScanFolder("default"); err != nil {
467		t.Fatal("Failed scanning:", err)
468	}
469
470	select {
471	case <-time.After(10 * time.Second):
472		t.Fatal("timed out")
473	case r := <-received:
474		f := checkReceived(r)
475		if !f.Version.Equal(protocol.Vector{}) {
476			t.Errorf("Got Version == %v, expected empty version", f.Version)
477		}
478	}
479}
480
481func TestRescanIfHaveInvalidContent(t *testing.T) {
482	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
483	defer wcfgCancel()
484	tfs := fcfg.Filesystem()
485	defer cleanupModelAndRemoveDir(m, tfs.URI())
486
487	payload := []byte("hello")
488
489	must(t, writeFile(tfs, "foo", payload, 0777))
490
491	received := make(chan []protocol.FileInfo)
492	fc.setIndexFn(func(_ context.Context, _ string, fs []protocol.FileInfo) error {
493		received <- fs
494		return nil
495	})
496	checkReceived := func(fs []protocol.FileInfo) protocol.FileInfo {
497		t.Helper()
498		if len(fs) != 1 {
499			t.Fatalf("Sent index with %d files, should be 1", len(fs))
500		}
501		if fs[0].Name != "foo" {
502			t.Fatalf(`Sent index with file %v, should be "foo"`, fs[0].Name)
503		}
504		return fs[0]
505	}
506
507	// Scan without ignore patterns with "foo" not existing locally
508	if err := m.ScanFolder("default"); err != nil {
509		t.Fatal("Failed scanning:", err)
510	}
511
512	f := checkReceived(<-received)
513	if f.Blocks[0].WeakHash != 103547413 {
514		t.Fatalf("unexpected weak hash: %d != 103547413", f.Blocks[0].WeakHash)
515	}
516
517	res, err := m.Request(device1, "default", "foo", 0, int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
518	if err != nil {
519		t.Fatal(err)
520	}
521	buf := res.Data()
522	if !bytes.Equal(buf, payload) {
523		t.Errorf("%s != %s", buf, payload)
524	}
525
526	payload = []byte("bye")
527	buf = make([]byte, len(payload))
528
529	must(t, writeFile(tfs, "foo", payload, 0777))
530
531	_, err = m.Request(device1, "default", "foo", 0, int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
532	if err == nil {
533		t.Fatalf("expected failure")
534	}
535
536	select {
537	case fs := <-received:
538		f := checkReceived(fs)
539		if f.Blocks[0].WeakHash != 41943361 {
540			t.Fatalf("unexpected weak hash: %d != 41943361", f.Blocks[0].WeakHash)
541		}
542	case <-time.After(time.Second):
543		t.Fatalf("timed out")
544	}
545}
546
547func TestParentDeletion(t *testing.T) {
548	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
549	defer wcfgCancel()
550	testFs := fcfg.Filesystem()
551	defer cleanupModelAndRemoveDir(m, testFs.URI())
552
553	parent := "foo"
554	child := filepath.Join(parent, "bar")
555
556	received := make(chan []protocol.FileInfo)
557	fc.addFile(parent, 0777, protocol.FileInfoTypeDirectory, nil)
558	fc.addFile(child, 0777, protocol.FileInfoTypeDirectory, nil)
559	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
560		received <- fs
561		return nil
562	})
563	fc.sendIndexUpdate()
564
565	// Get back index from initial setup
566	select {
567	case fs := <-received:
568		if len(fs) != 2 {
569			t.Fatalf("Sent index with %d files, should be 2", len(fs))
570		}
571	case <-time.After(time.Second):
572		t.Fatalf("timed out")
573	}
574
575	// Delete parent dir
576	must(t, testFs.RemoveAll(parent))
577
578	// Scan only the child dir (not the parent)
579	if err := m.ScanFolderSubdirs("default", []string{child}); err != nil {
580		t.Fatal("Failed scanning:", err)
581	}
582
583	select {
584	case fs := <-received:
585		if len(fs) != 1 {
586			t.Fatalf("Sent index with %d files, should be 1", len(fs))
587		}
588		if fs[0].Name != child {
589			t.Fatalf(`Sent index with file "%v", should be "%v"`, fs[0].Name, child)
590		}
591	case <-time.After(time.Second):
592		t.Fatalf("timed out")
593	}
594
595	// Recreate the child dir on the remote
596	fc.updateFile(child, 0777, protocol.FileInfoTypeDirectory, nil)
597	fc.sendIndexUpdate()
598
599	// Wait for the child dir to be recreated and sent to the remote
600	select {
601	case fs := <-received:
602		l.Debugln("sent:", fs)
603		found := false
604		for _, f := range fs {
605			if f.Name == child {
606				if f.Deleted {
607					t.Fatalf(`File "%v" is still deleted`, child)
608				}
609				found = true
610			}
611		}
612		if !found {
613			t.Fatalf(`File "%v" is missing in index`, child)
614		}
615	case <-time.After(5 * time.Second):
616		t.Fatalf("timed out")
617	}
618}
619
620// TestRequestSymlinkWindows checks that symlinks aren't marked as deleted on windows
621// Issue: https://github.com/syncthing/syncthing/issues/5125
622func TestRequestSymlinkWindows(t *testing.T) {
623	if runtime.GOOS != "windows" {
624		t.Skip("windows specific test")
625	}
626
627	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
628	defer wcfgCancel()
629	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
630
631	received := make(chan []protocol.FileInfo)
632	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
633		select {
634		case <-received:
635			t.Error("More than one index update sent")
636		default:
637		}
638		received <- fs
639		return nil
640	})
641
642	fc.addFile("link", 0644, protocol.FileInfoTypeSymlink, nil)
643	fc.sendIndexUpdate()
644
645	select {
646	case fs := <-received:
647		close(received)
648		// expected first index
649		if len(fs) != 1 {
650			t.Fatalf("Expected just one file in index, got %v", fs)
651		}
652		f := fs[0]
653		if f.Name != "link" {
654			t.Fatalf(`Got file info with path "%v", expected "link"`, f.Name)
655		}
656		if !f.IsInvalid() {
657			t.Errorf(`File info was not marked as invalid`)
658		}
659	case <-time.After(time.Second):
660		t.Fatalf("timed out before pull was finished")
661	}
662
663	sub := m.evLogger.Subscribe(events.StateChanged | events.LocalIndexUpdated)
664	defer sub.Unsubscribe()
665
666	m.ScanFolder("default")
667
668	for {
669		select {
670		case ev := <-sub.C():
671			switch data := ev.Data.(map[string]interface{}); {
672			case ev.Type == events.LocalIndexUpdated:
673				t.Fatalf("Local index was updated unexpectedly: %v", data)
674			case ev.Type == events.StateChanged:
675				if data["from"] == "scanning" {
676					return
677				}
678			}
679		case <-time.After(5 * time.Second):
680			t.Fatalf("Timed out before scan finished")
681		}
682	}
683}
684
685func equalContents(path string, contents []byte) error {
686	if bs, err := ioutil.ReadFile(path); err != nil {
687		return err
688	} else if !bytes.Equal(bs, contents) {
689		return errors.New("incorrect data")
690	}
691	return nil
692}
693
694func TestRequestRemoteRenameChanged(t *testing.T) {
695	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
696	defer wcfgCancel()
697	tfs := fcfg.Filesystem()
698	tmpDir := tfs.URI()
699	defer cleanupModelAndRemoveDir(m, tfs.URI())
700
701	received := make(chan []protocol.FileInfo)
702	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
703		select {
704		case <-received:
705			t.Error("More than one index update sent")
706		default:
707		}
708		received <- fs
709		return nil
710	})
711
712	// setup
713	a := "a"
714	b := "b"
715	data := map[string][]byte{
716		a: []byte("aData"),
717		b: []byte("bData"),
718	}
719	for _, n := range [2]string{a, b} {
720		fc.addFile(n, 0644, protocol.FileInfoTypeFile, data[n])
721	}
722	fc.sendIndexUpdate()
723	select {
724	case fs := <-received:
725		close(received)
726		if len(fs) != 2 {
727			t.Fatalf("Received index with %v indexes instead of 2", len(fs))
728		}
729	case <-time.After(10 * time.Second):
730		t.Fatal("timed out")
731	}
732
733	for _, n := range [2]string{a, b} {
734		must(t, equalContents(filepath.Join(tmpDir, n), data[n]))
735	}
736
737	var gotA, gotB, gotConfl bool
738	done := make(chan struct{})
739	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
740		select {
741		case <-done:
742			t.Error("Received more index updates than expected")
743			return nil
744		default:
745		}
746		for _, f := range fs {
747			switch {
748			case f.Name == a:
749				if gotA {
750					t.Error("Got more than one index update for", f.Name)
751				}
752				gotA = true
753			case f.Name == b:
754				if gotB {
755					t.Error("Got more than one index update for", f.Name)
756				}
757				if f.Version.Counter(fc.id.Short()) == 0 {
758					// This index entry might be superseeded
759					// by the final one or sent before it separately.
760					break
761				}
762				gotB = true
763			case strings.HasPrefix(f.Name, "b.sync-conflict-"):
764				if gotConfl {
765					t.Error("Got more than one index update for conflicts of", f.Name)
766				}
767				gotConfl = true
768			default:
769				t.Error("Got unexpected file in index update", f.Name)
770			}
771		}
772		if gotA && gotB && gotConfl {
773			close(done)
774		}
775		return nil
776	})
777
778	fd, err := tfs.OpenFile(b, fs.OptReadWrite, 0644)
779	if err != nil {
780		t.Fatal(err)
781	}
782	otherData := []byte("otherData")
783	if _, err = fd.Write(otherData); err != nil {
784		t.Fatal(err)
785	}
786	fd.Close()
787
788	// rename
789	fc.deleteFile(a)
790	fc.updateFile(b, 0644, protocol.FileInfoTypeFile, data[a])
791	// Make sure the remote file for b is newer and thus stays global -> local conflict
792	fc.mut.Lock()
793	for i := range fc.files {
794		if fc.files[i].Name == b {
795			fc.files[i].ModifiedS += 100
796			break
797		}
798	}
799	fc.mut.Unlock()
800	fc.sendIndexUpdate()
801	select {
802	case <-done:
803	case <-time.After(10 * time.Second):
804		t.Errorf("timed out without receiving all expected index updates")
805	}
806
807	// Check outcome
808	tfs.Walk(".", func(path string, info fs.FileInfo, err error) error {
809		switch {
810		case path == a:
811			t.Errorf(`File "a" was not removed`)
812		case path == b:
813			if err := equalContents(filepath.Join(tmpDir, b), data[a]); err != nil {
814				t.Error(`File "b" has unexpected content (renamed from a on remote)`)
815			}
816		case strings.HasPrefix(path, b+".sync-conflict-"):
817			if err := equalContents(filepath.Join(tmpDir, path), otherData); err != nil {
818				t.Error(`Sync conflict of "b" has unexptected content`)
819			}
820		case path == "." || strings.HasPrefix(path, ".stfolder"):
821		default:
822			t.Error("Found unexpected file", path)
823		}
824		return nil
825	})
826}
827
828func TestRequestRemoteRenameConflict(t *testing.T) {
829	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
830	defer wcfgCancel()
831	tfs := fcfg.Filesystem()
832	tmpDir := tfs.URI()
833	defer cleanupModelAndRemoveDir(m, tmpDir)
834
835	recv := make(chan int)
836	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
837		recv <- len(fs)
838		return nil
839	})
840
841	// setup
842	a := "a"
843	b := "b"
844	data := map[string][]byte{
845		a: []byte("aData"),
846		b: []byte("bData"),
847	}
848	for _, n := range [2]string{a, b} {
849		fc.addFile(n, 0644, protocol.FileInfoTypeFile, data[n])
850	}
851	fc.sendIndexUpdate()
852	select {
853	case i := <-recv:
854		if i != 2 {
855			t.Fatalf("received %v items in index, expected 1", i)
856		}
857	case <-time.After(10 * time.Second):
858		t.Fatal("timed out")
859	}
860
861	for _, n := range [2]string{a, b} {
862		must(t, equalContents(filepath.Join(tmpDir, n), data[n]))
863	}
864
865	fd, err := tfs.OpenFile(b, fs.OptReadWrite, 0644)
866	if err != nil {
867		t.Fatal(err)
868	}
869	otherData := []byte("otherData")
870	if _, err = fd.Write(otherData); err != nil {
871		t.Fatal(err)
872	}
873	fd.Close()
874	m.ScanFolders()
875	select {
876	case i := <-recv:
877		if i != 1 {
878			t.Fatalf("received %v items in index, expected 1", i)
879		}
880	case <-time.After(10 * time.Second):
881		t.Fatal("timed out")
882	}
883
884	// make sure the following rename is more recent (not concurrent)
885	time.Sleep(2 * time.Second)
886
887	// rename
888	fc.deleteFile(a)
889	fc.updateFile(b, 0644, protocol.FileInfoTypeFile, data[a])
890	fc.sendIndexUpdate()
891	select {
892	case <-recv:
893	case <-time.After(10 * time.Second):
894		t.Fatal("timed out")
895	}
896
897	// Check outcome
898	foundB := false
899	foundBConfl := false
900	tfs.Walk(".", func(path string, info fs.FileInfo, err error) error {
901		switch {
902		case path == a:
903			t.Errorf(`File "a" was not removed`)
904		case path == b:
905			foundB = true
906		case strings.HasPrefix(path, b) && strings.Contains(path, ".sync-conflict-"):
907			foundBConfl = true
908		}
909		return nil
910	})
911	if !foundB {
912		t.Errorf(`File "b" was removed`)
913	}
914	if !foundBConfl {
915		t.Errorf(`No conflict file for "b" was created`)
916	}
917}
918
919func TestRequestDeleteChanged(t *testing.T) {
920	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
921	defer wcfgCancel()
922	tfs := fcfg.Filesystem()
923	defer cleanupModelAndRemoveDir(m, tfs.URI())
924
925	done := make(chan struct{})
926	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
927		select {
928		case <-done:
929			t.Error("More than one index update sent")
930		default:
931		}
932		close(done)
933		return nil
934	})
935
936	// setup
937	a := "a"
938	data := []byte("aData")
939	fc.addFile(a, 0644, protocol.FileInfoTypeFile, data)
940	fc.sendIndexUpdate()
941	select {
942	case <-done:
943		done = make(chan struct{})
944	case <-time.After(10 * time.Second):
945		t.Fatal("timed out")
946	}
947
948	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
949		select {
950		case <-done:
951			t.Error("More than one index update sent")
952		default:
953		}
954		close(done)
955		return nil
956	})
957
958	fd, err := tfs.OpenFile(a, fs.OptReadWrite, 0644)
959	if err != nil {
960		t.Fatal(err)
961	}
962	otherData := []byte("otherData")
963	if _, err = fd.Write(otherData); err != nil {
964		t.Fatal(err)
965	}
966	fd.Close()
967
968	// rename
969	fc.deleteFile(a)
970	fc.sendIndexUpdate()
971	select {
972	case <-done:
973	case <-time.After(10 * time.Second):
974		t.Fatal("timed out")
975	}
976
977	// Check outcome
978	if _, err := tfs.Lstat(a); err != nil {
979		if fs.IsNotExist(err) {
980			t.Error(`Modified file "a" was removed`)
981		} else {
982			t.Error(`Error stating file "a":`, err)
983		}
984	}
985}
986
987func TestNeedFolderFiles(t *testing.T) {
988	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
989	defer wcfgCancel()
990	tfs := fcfg.Filesystem()
991	tmpDir := tfs.URI()
992	defer cleanupModelAndRemoveDir(m, tmpDir)
993
994	sub := m.evLogger.Subscribe(events.RemoteIndexUpdated)
995	defer sub.Unsubscribe()
996
997	errPreventSync := errors.New("you aren't getting any of this")
998	fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
999		return nil, errPreventSync
1000	})
1001
1002	data := []byte("foo")
1003	num := 20
1004	for i := 0; i < num; i++ {
1005		fc.addFile(strconv.Itoa(i), 0644, protocol.FileInfoTypeFile, data)
1006	}
1007	fc.sendIndexUpdate()
1008
1009	select {
1010	case <-sub.C():
1011	case <-time.After(5 * time.Second):
1012		t.Fatal("Timed out before receiving index")
1013	}
1014
1015	progress, queued, rest, err := m.NeedFolderFiles(fcfg.ID, 1, 100)
1016	must(t, err)
1017	if got := len(progress) + len(queued) + len(rest); got != num {
1018		t.Errorf("Got %v needed items, expected %v", got, num)
1019	}
1020
1021	exp := 10
1022	for page := 1; page < 3; page++ {
1023		progress, queued, rest, err := m.NeedFolderFiles(fcfg.ID, page, exp)
1024		must(t, err)
1025		if got := len(progress) + len(queued) + len(rest); got != exp {
1026			t.Errorf("Got %v needed items on page %v, expected %v", got, page, exp)
1027		}
1028	}
1029}
1030
1031// TestIgnoreDeleteUnignore checks that the deletion of an ignored file is not
1032// propagated upon un-ignoring.
1033// https://github.com/syncthing/syncthing/issues/6038
1034func TestIgnoreDeleteUnignore(t *testing.T) {
1035	w, fcfg, wCancel := tmpDefaultWrapper()
1036	defer wCancel()
1037	m := setupModel(t, w)
1038	fss := fcfg.Filesystem()
1039	tmpDir := fss.URI()
1040	defer cleanupModelAndRemoveDir(m, tmpDir)
1041
1042	folderIgnoresAlwaysReload(t, m, fcfg)
1043	m.ScanFolders()
1044
1045	fc := addFakeConn(m, device1, fcfg.ID)
1046	fc.folder = "default"
1047	fc.mut.Lock()
1048	fc.mut.Unlock()
1049
1050	file := "foobar"
1051	contents := []byte("test file contents\n")
1052
1053	basicCheck := func(fs []protocol.FileInfo) {
1054		t.Helper()
1055		if len(fs) != 1 {
1056			t.Fatal("expected a single index entry, got", len(fs))
1057		} else if fs[0].Name != file {
1058			t.Fatalf("expected a index entry for %v, got one for %v", file, fs[0].Name)
1059		}
1060	}
1061
1062	done := make(chan struct{})
1063	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
1064		basicCheck(fs)
1065		close(done)
1066		return nil
1067	})
1068
1069	if err := writeFile(fss, file, contents, 0644); err != nil {
1070		panic(err)
1071	}
1072	m.ScanFolders()
1073
1074	select {
1075	case <-time.After(5 * time.Second):
1076		t.Fatalf("timed out before index was received")
1077	case <-done:
1078	}
1079
1080	done = make(chan struct{})
1081	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
1082		basicCheck(fs)
1083		f := fs[0]
1084		if !f.IsInvalid() {
1085			t.Errorf("Received non-invalid index update")
1086		}
1087		close(done)
1088		return nil
1089	})
1090
1091	if err := m.SetIgnores("default", []string{"foobar"}); err != nil {
1092		panic(err)
1093	}
1094
1095	select {
1096	case <-time.After(5 * time.Second):
1097		t.Fatal("timed out before receiving index update")
1098	case <-done:
1099	}
1100
1101	done = make(chan struct{})
1102	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
1103		basicCheck(fs)
1104		f := fs[0]
1105		if f.IsInvalid() {
1106			t.Errorf("Received invalid index update")
1107		}
1108		if !f.Version.Equal(protocol.Vector{}) && f.Deleted {
1109			t.Error("Received deleted index entry with non-empty version")
1110		}
1111		l.Infoln(f)
1112		close(done)
1113		return nil
1114	})
1115
1116	if err := fss.Remove(file); err != nil {
1117		t.Fatal(err)
1118	}
1119	if err := m.SetIgnores("default", []string{}); err != nil {
1120		panic(err)
1121	}
1122
1123	select {
1124	case <-time.After(5 * time.Second):
1125		t.Fatalf("timed out before index was received")
1126	case <-done:
1127	}
1128}
1129
1130// TestRequestLastFileProgress checks that the last pulled file (here only) is registered
1131// as in progress.
1132func TestRequestLastFileProgress(t *testing.T) {
1133	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
1134	defer wcfgCancel()
1135	tfs := fcfg.Filesystem()
1136	defer cleanupModelAndRemoveDir(m, tfs.URI())
1137
1138	done := make(chan struct{})
1139
1140	fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
1141		defer close(done)
1142		progress, queued, rest, err := m.NeedFolderFiles(folder, 1, 10)
1143		must(t, err)
1144		if len(queued)+len(rest) != 0 {
1145			t.Error(`There should not be any queued or "rest" items`)
1146		}
1147		if len(progress) != 1 {
1148			t.Error("Expected exactly one item in progress.")
1149		}
1150		return fc.fileData[name], nil
1151	})
1152
1153	contents := []byte("test file contents\n")
1154	fc.addFile("testfile", 0644, protocol.FileInfoTypeFile, contents)
1155	fc.sendIndexUpdate()
1156
1157	select {
1158	case <-done:
1159	case <-time.After(5 * time.Second):
1160		t.Fatal("Timed out before file was requested")
1161	}
1162}
1163
1164func TestRequestIndexSenderPause(t *testing.T) {
1165	done := make(chan struct{})
1166	defer close(done)
1167
1168	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
1169	defer wcfgCancel()
1170	tfs := fcfg.Filesystem()
1171	defer cleanupModelAndRemoveDir(m, tfs.URI())
1172
1173	indexChan := make(chan []protocol.FileInfo)
1174	fc.setIndexFn(func(ctx context.Context, folder string, fs []protocol.FileInfo) error {
1175		select {
1176		case indexChan <- fs:
1177		case <-done:
1178		case <-ctx.Done():
1179		}
1180		return nil
1181	})
1182
1183	var seq int64 = 1
1184	files := []protocol.FileInfo{{Name: "foo", Size: 10, Version: protocol.Vector{}.Update(myID.Short()), Sequence: seq}}
1185
1186	// Both devices connected, noone paused
1187	localIndexUpdate(m, fcfg.ID, files)
1188	select {
1189	case <-time.After(5 * time.Second):
1190		t.Fatal("timed out before receiving index")
1191	case <-indexChan:
1192	}
1193
1194	// Remote paused
1195
1196	cc := basicClusterConfig(device1, myID, fcfg.ID)
1197	cc.Folders[0].Paused = true
1198	m.ClusterConfig(device1, cc)
1199
1200	seq++
1201	files[0].Sequence = seq
1202	files[0].Version = files[0].Version.Update(myID.Short())
1203	localIndexUpdate(m, fcfg.ID, files)
1204
1205	// I don't see what to hook into to ensure an index update is not sent.
1206	dur := 50 * time.Millisecond
1207	if !testing.Short() {
1208		dur = 2 * time.Second
1209	}
1210	select {
1211	case <-time.After(dur):
1212	case <-indexChan:
1213		t.Error("Received index despite remote being paused")
1214	}
1215
1216	// Remote unpaused
1217
1218	cc.Folders[0].Paused = false
1219	m.ClusterConfig(device1, cc)
1220	select {
1221	case <-time.After(5 * time.Second):
1222		t.Fatal("timed out before receiving index")
1223	case <-indexChan:
1224	}
1225
1226	// Local paused and resume
1227
1228	pauseFolder(t, m.cfg, fcfg.ID, true)
1229	pauseFolder(t, m.cfg, fcfg.ID, false)
1230
1231	seq++
1232	files[0].Sequence = seq
1233	files[0].Version = files[0].Version.Update(myID.Short())
1234	localIndexUpdate(m, fcfg.ID, files)
1235	select {
1236	case <-time.After(5 * time.Second):
1237		t.Fatal("timed out before receiving index")
1238	case <-indexChan:
1239	}
1240
1241	// Local and remote paused, then first resume remote, then local
1242
1243	cc.Folders[0].Paused = true
1244	m.ClusterConfig(device1, cc)
1245
1246	pauseFolder(t, m.cfg, fcfg.ID, true)
1247
1248	cc.Folders[0].Paused = false
1249	m.ClusterConfig(device1, cc)
1250
1251	pauseFolder(t, m.cfg, fcfg.ID, false)
1252
1253	seq++
1254	files[0].Sequence = seq
1255	files[0].Version = files[0].Version.Update(myID.Short())
1256	localIndexUpdate(m, fcfg.ID, files)
1257	select {
1258	case <-time.After(5 * time.Second):
1259		t.Fatal("timed out before receiving index")
1260	case <-indexChan:
1261	}
1262
1263	// Folder removed on remote
1264
1265	cc = protocol.ClusterConfig{}
1266	m.ClusterConfig(device1, cc)
1267
1268	seq++
1269	files[0].Sequence = seq
1270	files[0].Version = files[0].Version.Update(myID.Short())
1271	localIndexUpdate(m, fcfg.ID, files)
1272
1273	select {
1274	case <-time.After(dur):
1275	case <-indexChan:
1276		t.Error("Received index despite remote not having the folder")
1277	}
1278}
1279
1280func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
1281	w, fcfg, wCancel := tmpDefaultWrapper()
1282	defer wCancel()
1283	tfs := fcfg.Filesystem()
1284	dir1 := "foo"
1285	dir2 := "bar"
1286
1287	// Initialise db with an entry and then stop everything again
1288	must(t, tfs.Mkdir(dir1, 0777))
1289	m := newModel(t, w, myID, "syncthing", "dev", nil)
1290	defer cleanupModelAndRemoveDir(m, tfs.URI())
1291	m.ServeBackground()
1292	m.ScanFolders()
1293	m.cancel()
1294	<-m.stopped
1295
1296	// Add connection (sends incoming cluster config) before starting the new model
1297	m = &testModel{
1298		model:    NewModel(m.cfg, m.id, m.clientName, m.clientVersion, m.db, m.protectedFiles, m.evLogger).(*model),
1299		evCancel: m.evCancel,
1300		stopped:  make(chan struct{}),
1301	}
1302	defer cleanupModel(m)
1303	fc := addFakeConn(m, device1, fcfg.ID)
1304	done := make(chan struct{})
1305	defer close(done) // Must be the last thing to be deferred, thus first to run.
1306	indexChan := make(chan []protocol.FileInfo, 1)
1307	ccChan := make(chan protocol.ClusterConfig, 1)
1308	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
1309		select {
1310		case indexChan <- fs:
1311		case <-done:
1312		}
1313		return nil
1314	})
1315	fc.ClusterConfigCalls(func(cc protocol.ClusterConfig) {
1316		select {
1317		case ccChan <- cc:
1318		case <-done:
1319		}
1320	})
1321
1322	m.ServeBackground()
1323
1324	timeout := time.After(5 * time.Second)
1325
1326	// Check that cluster-config is resent after adding folders when starting model
1327	select {
1328	case <-timeout:
1329		t.Fatal("timed out before receiving cluster-config")
1330	case <-ccChan:
1331	}
1332
1333	// Check that an index is sent for the newly added item
1334	must(t, tfs.Mkdir(dir2, 0777))
1335	m.ScanFolders()
1336	select {
1337	case <-timeout:
1338		t.Fatal("timed out before receiving index")
1339	case <-indexChan:
1340	}
1341}
1342
1343func TestRequestReceiveEncrypted(t *testing.T) {
1344	if testing.Short() {
1345		t.Skip("skipping on short testing - scrypt is too slow")
1346	}
1347
1348	w, fcfg, wCancel := tmpDefaultWrapper()
1349	defer wCancel()
1350	tfs := fcfg.Filesystem()
1351	fcfg.Type = config.FolderTypeReceiveEncrypted
1352	setFolder(t, w, fcfg)
1353
1354	encToken := protocol.PasswordToken(fcfg.ID, "pw")
1355	must(t, tfs.Mkdir(config.DefaultMarkerName, 0777))
1356	must(t, writeEncryptionToken(encToken, fcfg))
1357
1358	m := setupModel(t, w)
1359	defer cleanupModelAndRemoveDir(m, tfs.URI())
1360
1361	files := genFiles(2)
1362	files[1].LocalFlags = protocol.FlagLocalReceiveOnly
1363	m.fmut.RLock()
1364	fset := m.folderFiles[fcfg.ID]
1365	m.fmut.RUnlock()
1366	fset.Update(protocol.LocalDeviceID, files)
1367
1368	indexChan := make(chan []protocol.FileInfo, 10)
1369	done := make(chan struct{})
1370	defer close(done)
1371	fc := newFakeConnection(device1, m)
1372	fc.folder = fcfg.ID
1373	fc.setIndexFn(func(_ context.Context, _ string, fs []protocol.FileInfo) error {
1374		select {
1375		case indexChan <- fs:
1376		case <-done:
1377		}
1378		return nil
1379	})
1380	m.AddConnection(fc, protocol.Hello{})
1381	m.ClusterConfig(device1, protocol.ClusterConfig{
1382		Folders: []protocol.Folder{
1383			{
1384				ID: "default",
1385				Devices: []protocol.Device{
1386					{
1387						ID:                      myID,
1388						EncryptionPasswordToken: encToken,
1389					},
1390					{ID: device1},
1391				},
1392			},
1393		},
1394	})
1395
1396	select {
1397	case fs := <-indexChan:
1398		if len(fs) != 1 {
1399			t.Error("Expected index with one file, got", fs)
1400		}
1401		if got := fs[0].Name; got != files[0].Name {
1402			t.Errorf("Expected file %v, got %v", got, files[0].Name)
1403		}
1404	case <-time.After(5 * time.Second):
1405		t.Fatal("timed out before receiving index")
1406	}
1407
1408	// Detects deletion, as we never really created the file on disk
1409	// Shouldn't send anything because receive-encrypted
1410	must(t, m.ScanFolder(fcfg.ID))
1411	// One real file to be sent
1412	name := "foo"
1413	data := make([]byte, 2000)
1414	rand.Read(data)
1415	fc.addFile(name, 0664, protocol.FileInfoTypeFile, data)
1416	fc.sendIndexUpdate()
1417
1418	select {
1419	case fs := <-indexChan:
1420		if len(fs) != 1 {
1421			t.Error("Expected index with one file, got", fs)
1422		}
1423		if got := fs[0].Name; got != name {
1424			t.Errorf("Expected file %v, got %v", got, files[0].Name)
1425		}
1426	case <-time.After(5 * time.Second):
1427		t.Fatal("timed out before receiving index")
1428	}
1429
1430	// Simulate request from device that is untrusted too, i.e. with non-empty, but garbage hash
1431	_, err := m.Request(device1, fcfg.ID, name, 0, 1064, 0, []byte("garbage"), 0, false)
1432	must(t, err)
1433}
1434
1435func TestRequestGlobalInvalidToValid(t *testing.T) {
1436	done := make(chan struct{})
1437	defer close(done)
1438
1439	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
1440	defer wcfgCancel()
1441	fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2})
1442	waiter, err := m.cfg.Modify(func(cfg *config.Configuration) {
1443		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device2, "device2"))
1444		fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2})
1445		cfg.SetFolder(fcfg)
1446	})
1447	must(t, err)
1448	waiter.Wait()
1449	addFakeConn(m, device2, fcfg.ID)
1450	tfs := fcfg.Filesystem()
1451	defer cleanupModelAndRemoveDir(m, tfs.URI())
1452
1453	indexChan := make(chan []protocol.FileInfo, 1)
1454	fc.setIndexFn(func(ctx context.Context, folder string, fs []protocol.FileInfo) error {
1455		select {
1456		case indexChan <- fs:
1457		case <-done:
1458		case <-ctx.Done():
1459		}
1460		return nil
1461	})
1462
1463	name := "foo"
1464
1465	// Setup device with valid file, do not send index yet
1466	contents := []byte("test file contents\n")
1467	fc.addFile(name, 0644, protocol.FileInfoTypeFile, contents)
1468
1469	// Third device ignoring the same file
1470	fc.mut.Lock()
1471	file := fc.files[0]
1472	fc.mut.Unlock()
1473	file.SetIgnored()
1474	m.IndexUpdate(device2, fcfg.ID, []protocol.FileInfo{prepareFileInfoForIndex(file)})
1475
1476	// Wait for the ignored file to be received and possible pulled
1477	timeout := time.After(10 * time.Second)
1478	globalUpdated := false
1479	for {
1480		select {
1481		case <-timeout:
1482			t.Fatalf("timed out (globalUpdated == %v)", globalUpdated)
1483		default:
1484			time.Sleep(10 * time.Millisecond)
1485		}
1486		if !globalUpdated {
1487			_, ok, err := m.CurrentGlobalFile(fcfg.ID, name)
1488			if err != nil {
1489				t.Fatal(err)
1490			}
1491			if !ok {
1492				continue
1493			}
1494			globalUpdated = true
1495		}
1496		snap, err := m.DBSnapshot(fcfg.ID)
1497		if err != nil {
1498			t.Fatal(err)
1499		}
1500		need := snap.NeedSize(protocol.LocalDeviceID)
1501		snap.Release()
1502		if need.Files == 0 {
1503			break
1504		}
1505	}
1506
1507	// Send the valid file
1508	fc.sendIndexUpdate()
1509
1510	gotInvalid := false
1511	for {
1512		select {
1513		case <-timeout:
1514			t.Fatal("timed out before receiving index")
1515		case fs := <-indexChan:
1516			if len(fs) != 1 {
1517				t.Fatalf("Expected one file in index, got %v", len(fs))
1518			}
1519			if !fs[0].IsInvalid() {
1520				return
1521			}
1522			if gotInvalid {
1523				t.Fatal("Received two invalid index updates")
1524			}
1525			t.Log("got index with invalid file")
1526			gotInvalid = true
1527		}
1528	}
1529}
1530