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