Exemple #1
0
        private void HandleJoinTeamCollection(ApiRequest request)
        {
            try
            {
                FolderTeamCollection.JoinCollectionTeam();
                ReactDialog.CloseCurrentModal();

                Analytics.Track("TeamCollectionJoin",
                                new Dictionary <string, string>()
                {
                    { "CollectionId", _settings?.CollectionId },
                    { "CollectionName", _settings?.CollectionName },
                    { "Backend", _tcManager?.CurrentCollection?.GetBackendType() },
                    { "User", CurrentUser }
                });

                request.PostSucceeded();
            }
            catch (Exception e)
            {
                // Not sure what to do here: joining the collection crashed.
                Logger.WriteError("TeamCollectionApi.HandleJoinTeamCollection() crashed", e);
                var msg = LocalizationManager.GetString("TeamCollection.ErrorJoining", "Could not join Team Collection");
                ErrorReport.NotifyUserOfProblem(e, msg);
                NonFatalProblem.ReportSentryOnly(e, $"Something went wrong for {request.LocalPath()}");

                // Since we have already informed the user above, it is better to just report a success here.
                // Otherwise, they will also get a toast.
                request.PostSucceeded();
            }
        }
 public void Checkin_RenamedBook_DeletesOriginal_NoTombstone()
 {
     using (var collectionFolder =
                new TemporaryFolder("Checkin_RenamedBook_DeletesOriginal_Collection"))
     {
         using (var repoFolder =
                    new TemporaryFolder("Checkin_RenamedBook_DeletesOriginal_Shared"))
         {
             var mockTcManager = new Mock <ITeamCollectionManager>();
             TeamCollectionManager.ForceCurrentUserForTests("*****@*****.**");
             var tc = new FolderTeamCollection(mockTcManager.Object, collectionFolder.FolderPath,
                                               repoFolder.FolderPath);
             tc.CollectionId = Bloom.TeamCollection.TeamCollection.GenerateCollectionId();
             var oldFolderPath =
                 SyncAtStartupTests.MakeFakeBook(collectionFolder.FolderPath, "old name", "book content");
             tc.PutBook(oldFolderPath);
             tc.AttemptLock("old name");
             SyncAtStartupTests.SimulateRename(tc, "old name", "middle name");
             SyncAtStartupTests.SimulateRename(tc, "middle name", "new name");
             tc.PutBook(Path.Combine(collectionFolder.FolderPath, "new name"), true);
             Assert.That(File.Exists(tc.GetPathToBookFileInRepo("new name")), Is.True);
             Assert.That(File.Exists(tc.GetPathToBookFileInRepo("old name")), Is.False,
                         "old name was not deleted");
             var status = tc.GetLocalStatus("new name");
             Assert.That(status.oldName ?? "", Is.Empty,
                         "Should stop tracking previous name once we cleaned it up");
             Assert.That(tc.KnownToHaveBeenDeleted("old name"), Is.False);
             TeamCollectionManager.ForceCurrentUserForTests(null);
         }
     }
 }
        public void ConnectToTeamCollection_SetsUpRequiredFiles()
        {
            using (var collectionFolder = new TemporaryFolder("FolderTeamCollectionTests2_Collection"))
            {
                using (var sharedFolder = new TemporaryFolder("FolderTeamCollectionTests2_Shared"))
                {
                    var bookFolderName1 = "Some book";
                    SyncAtStartupTests.MakeFakeBook(collectionFolder.FolderPath, bookFolderName1, "Something");
                    // BL-9573 tests cases where the book name isn't exactly the same as the folder name
                    var bookFolderName2 = "Some other book";
                    SyncAtStartupTests.MakeFakeBook(collectionFolder.FolderPath, "Some other name altogether",
                                                    "Strange book content", bookFolderName2);
                    var settingsFileName =
                        Path.ChangeExtension(Path.GetFileName(collectionFolder.FolderPath), "bloomCollection");
                    var settingsPath = Path.Combine(collectionFolder.FolderPath, settingsFileName);

                    // As an aside, this is a convenient place to check that a TC manager created when TC settings does not exist
                    // functions and does not have a current collection.
                    var tcManager = new TeamCollectionManager(settingsPath, null, new BookRenamedEvent(),
                                                              new BookStatusChangeEvent(), null, null);
                    Assert.That(tcManager.CurrentCollection, Is.Null);

                    RobustFile.WriteAllText(settingsPath, "This is a fake settings file");
                    FolderTeamCollection.CreateTeamCollectionLinkFile(collectionFolder.FolderPath,
                                                                      sharedFolder.FolderPath);

                    var nonBookFolder = Path.Combine(collectionFolder.FolderPath, "Some other folder");
                    Directory.CreateDirectory(nonBookFolder);
                    tcManager = new TeamCollectionManager(settingsPath, null, new BookRenamedEvent(),
                                                          new BookStatusChangeEvent(), null, null);
                    var collection = tcManager.CurrentCollection;

                    // sut
                    (collection as FolderTeamCollection)?.SetupTeamCollection(sharedFolder.FolderPath,
                                                                              new NullWebSocketProgress());

                    Assert.That(collection, Is.Not.Null);
                    var joinCollectionPath =
                        Path.Combine(sharedFolder.FolderPath, "Join this Team Collection.JoinBloomTC");
                    Assert.That(File.Exists(joinCollectionPath));

                    var teamCollectionLinkPath =
                        Path.Combine(collectionFolder.FolderPath, TeamCollectionManager.TeamCollectionLinkFileName);
                    Assert.That(File.Exists(teamCollectionLinkPath));
                    var collectionFileContent = RobustFile.ReadAllText(teamCollectionLinkPath);
                    Assert.That(collectionFileContent, Is.EqualTo(sharedFolder.FolderPath));
                    var sharedSettingsPath = Path.Combine(collectionFolder.FolderPath, settingsFileName);
                    Assert.That(RobustFile.ReadAllText(sharedSettingsPath), Is.EqualTo("This is a fake settings file"));
                    var bookPath = Path.Combine(sharedFolder.FolderPath, "Books", bookFolderName1 + ".bloom");
                    Assert.That(File.Exists(bookPath));
                    var bookPath2 = Path.Combine(sharedFolder.FolderPath, "Books", bookFolderName2 + ".bloom");
                    Assert.That(File.Exists(bookPath2));
                }
            }
        }
        public void OkToCheckIn_GivesCorrectResults()
        {
            using (var collectionFolder =
                       new TemporaryFolder("OkToCheckIn_GivesCorrectResults_Collection"))
            {
                using (var repoFolder =
                           new TemporaryFolder("OkToCheckIn_GivesCorrectResults_Shared"))
                {
                    var mockTcManager = new Mock <ITeamCollectionManager>();
                    TeamCollectionManager.ForceCurrentUserForTests("");
                    var tc = new FolderTeamCollection(mockTcManager.Object, collectionFolder.FolderPath,
                                                      repoFolder.FolderPath);
                    tc.CollectionId = Bloom.TeamCollection.TeamCollection.GenerateCollectionId();
                    var bookFolderPath =
                        SyncAtStartupTests.MakeFakeBook(collectionFolder.FolderPath, "some name", "book content");
                    Assert.That(tc.OkToCheckIn("some name"), Is.False, "can't check in new book when not registered");

                    TeamCollectionManager.ForceCurrentUserForTests("*****@*****.**");
                    Assert.That(tc.OkToCheckIn("some name"), Is.True, "can check in new book");

                    tc.PutBook(bookFolderPath, true);
                    tc.AttemptLock("some name");
                    Assert.That(tc.OkToCheckIn("some name"), Is.True,
                                "can check in unmodified book with normal checkout status");

                    TeamCollectionManager.ForceCurrentUserForTests("");
                    Assert.That(tc.OkToCheckIn("some name"), Is.False,
                                "normally permitted checkin is forbidden with no registration");
                    TeamCollectionManager.ForceCurrentUserForTests("*****@*****.**");

                    var status    = tc.GetStatus("some name");
                    var altStatus = status.WithChecksum("some random thing");
                    tc.WriteBookStatus("some name", altStatus);
                    tc.WriteLocalStatus("some name", status);
                    Assert.That(tc.OkToCheckIn("some name"), Is.False, "can't check in, mysteriously modified in repo");

                    altStatus = status.WithLockedBy(null);
                    tc.WriteBookStatus("some name", altStatus);
                    tc.WriteLocalStatus("some name", status);
                    Assert.That(tc.OkToCheckIn("some name"), Is.True,
                                "special case, repo has lost checkout status, but not locked or modified");

                    altStatus = status.WithLockedBy("*****@*****.**");
                    tc.WriteBookStatus("some name", altStatus);
                    tc.WriteLocalStatus("some name", status);
                    Assert.That(tc.OkToCheckIn("some name"), Is.False, "conflicting lock in repo");

                    TeamCollectionManager.ForceCurrentUserForTests("null");
                }
            }
        }
        public void Setup()
        {
            TeamCollectionManager.ForceCurrentUserForTests("*****@*****.**");

            _sharedFolder     = new TemporaryFolder("TeamCollection_Shared");
            _collectionFolder = new TemporaryFolder("TeamCollection_Local");
            _tcLog            = new TeamCollectionMessageLog(TeamCollectionManager.GetTcLogPathFromLcPath(_collectionFolder.FolderPath));
            FolderTeamCollection.CreateTeamCollectionLinkFile(_collectionFolder.FolderPath,
                                                              _sharedFolder.FolderPath);

            _mockTcManager           = new Mock <ITeamCollectionManager>();
            _collection              = new FolderTeamCollection(_mockTcManager.Object, _collectionFolder.FolderPath, _sharedFolder.FolderPath, _tcLog);
            _collection.CollectionId = Bloom.TeamCollection.TeamCollection.GenerateCollectionId();
        }
        public void ChangeToFileInOther_RaisesRepoCollectionFilesChanged()
        {
            using (var collectionFolder =
                       new TemporaryFolder("ChangeToFileInOther_RaisesRepoCollectionFilesChanged"))
            {
                using (var repoFolder =
                           new TemporaryFolder("ChangeToFileInOther_RaisesRepoCollectionFilesChanged"))
                {
                    var mockTcManager = new Mock <ITeamCollectionManager>();
                    var tc            = new TestFolderTeamCollection(mockTcManager.Object, collectionFolder.FolderPath,
                                                                     repoFolder.FolderPath);
                    var otherPath = Path.Combine(collectionFolder.FolderPath,
                                                 Path.GetFileName(collectionFolder.FolderPath) + ".bloomCollection");
                    Directory.CreateDirectory(Path.GetDirectoryName(otherPath));
                    // this test doesn't need this folder except that StartMonitoring does.
                    Directory.CreateDirectory(Path.Combine(repoFolder.FolderPath, "Books"));
                    File.WriteAllText(otherPath, "This is the initial value");
                    tc.CopyRepoCollectionFilesFromLocal(collectionFolder.FolderPath);

                    var eventWasRaised = false;

                    tc.SetupMonitoringBehavior();
                    ManualResetEvent         collectionChangedRaised = new ManualResetEvent(false);
                    EventHandler <EventArgs> monitorFunction         = (sender, args) =>
                    {
                        eventWasRaised = true;
                        collectionChangedRaised.Set();
                    };
                    tc.RepoCollectionFilesChanged += monitorFunction;

                    // sut (at least, triggers it and waits for it)
                    Thread.Sleep(10);
                    var otherRepoPath = FolderTeamCollection.GetRepoProjectFilesZipPath(repoFolder.FolderPath);
                    RobustFile.WriteAllText(otherRepoPath, @"This is changed");                     // no, not a zip at all

                    var waitSucceeded = collectionChangedRaised.WaitOne(1000);

                    // To avoid messing up other tests, clean up before asserting.
                    tc.RepoCollectionFilesChanged -= monitorFunction;
                    tc.StopMonitoring();

                    Assert.That(eventWasRaised, Is.True, "event was not raised");
                }
            }
        }
        public void ConnectToTeamCollection(string repoFolderParentPath, string collectionId)
        {
            var repoFolderPath = PlannedRepoFolderPath(repoFolderParentPath);

            Directory.CreateDirectory(repoFolderPath);
            // The creator of a TC is its first and, for now, usually only administrator.
            // (Currently there is no way to change this except to hand-edit the file.)
            Settings.Administrators = new[] { CurrentUser };
            Settings.Save();
            var newTc = new FolderTeamCollection(this, _localCollectionFolder, repoFolderPath, bookCollectionHolder: _bookCollectionHolder);

            newTc.CollectionId = collectionId;
            newTc.SocketServer = SocketServer;
            newTc.TCManager    = this;
            newTc.SetupTeamCollectionWithProgressDialog(repoFolderPath);
            CurrentCollection = newTc;
            CurrentCollectionEvenIfDisconnected = newTc;
        }
Exemple #8
0
        public void OneTimeSetup()
        {
            _repoFolder       = new TemporaryFolder("FolderTeamCollectionTests_Repo");
            _collectionFolder = new TemporaryFolder("FolderTeamCollectionTests_Local");
            FolderTeamCollection.CreateTeamCollectionLinkFile(_collectionFolder.FolderPath,
                                                              _repoFolder.FolderPath);
            _mockTcManager = new Mock <ITeamCollectionManager>();
            _collection    = new TestFolderTeamCollection(_mockTcManager.Object, _collectionFolder.FolderPath, _repoFolder.FolderPath);
            // Some monitoring tests now require this to exist
            Directory.CreateDirectory(Path.Combine(_repoFolder.FolderPath, "Other"));

            // Make some books and check them in. Individual tests verify the results.
            // This book has an additional file, including a subfolder, to ensure they get
            // saved also.
            var bookFolderPath = Path.Combine(_collectionFolder.FolderPath, "My book");

            Directory.CreateDirectory(bookFolderPath);
            var bookPath = Path.Combine(bookFolderPath, "My book.htm");

            RobustFile.WriteAllText(bookPath, "This is just a dummy");
            var cssPath = Path.Combine(bookFolderPath, "BasicLayout.css");

            RobustFile.WriteAllText(cssPath, "This is another dummy");
            var audioDirectory = Path.Combine(bookFolderPath, "audio");

            Directory.CreateDirectory(audioDirectory);
            var mp3Path = Path.Combine(audioDirectory, "rubbish.mp3");

            RobustFile.WriteAllText(mp3Path, "Fake mp3");

            var secondBookFolderPath = Path.Combine(_collectionFolder.FolderPath, kAnotherBook);

            Directory.CreateDirectory(secondBookFolderPath);
            var anotherBookPath = Path.Combine(secondBookFolderPath, kAnotherBook + ".htm");

            RobustFile.WriteAllText(anotherBookPath, "This is just a dummy for another book");

            _myBookStatus      = _collection.PutBook(bookFolderPath);
            _anotherBookStatus = _collection.PutBook(secondBookFolderPath);

            // Also put the book (twice!) into lost and found
            _collection.PutBook(secondBookFolderPath, inLostAndFound: true);
            _collection.PutBook(secondBookFolderPath, inLostAndFound: true);
        }
 public void FilesToMonitorForCollection_NonStandardCollectionFileName_FindsIt()
 {
     using (var collectionFolder =
                new TemporaryFolder("SyncLocalAndRepoCollectionFiles_SyncsInRightDirection_Collection"))
     {
         using (var repoFolder =
                    new TemporaryFolder("SyncLocalAndRepoCollectionFiles_SyncsInRightDirection_Shared"))
         {
             var mockTcManager = new Mock <ITeamCollectionManager>();
             var tc            = new FolderTeamCollection(mockTcManager.Object, collectionFolder.FolderPath,
                                                          repoFolder.FolderPath);
             tc.CollectionId = Bloom.TeamCollection.TeamCollection.GenerateCollectionId();
             var bcPath = Path.Combine(collectionFolder.FolderPath, "mybooks.bloomCollection");
             File.WriteAllText(bcPath, "something");
             var files = tc.FilesToMonitorForCollection();
             Assert.That(files, Contains.Item(bcPath));
         }
     }
 }
        public void OneTimeSetup()
        {
            _repoFolder       = new TemporaryFolder("SyncAtStartup_Repo");
            _collectionFolder = new TemporaryFolder("SyncAtStartup_Local");
            FolderTeamCollection.CreateTeamCollectionLinkFile(_collectionFolder.FolderPath,
                                                              _repoFolder.FolderPath);
            _mockTcManager           = new Mock <ITeamCollectionManager>();
            _tcLog                   = new TeamCollectionMessageLog(TeamCollectionManager.GetTcLogPathFromLcPath(_collectionFolder.FolderPath));
            _collection              = new FolderTeamCollection(_mockTcManager.Object, _collectionFolder.FolderPath, _repoFolder.FolderPath, tcLog: _tcLog);
            _collection.CollectionId = Bloom.TeamCollection.TeamCollection.GenerateCollectionId();
            TeamCollectionManager.ForceCurrentUserForTests("*****@*****.**");

            // Simulate a book that was once shared, but has been deleted from the repo folder (has a tombstone).
            MakeBook("Should be deleted", "This should be deleted as it has local status but is not shared", true);
            var bookFolderPath = Path.Combine(_collectionFolder.FolderPath, "Should be deleted");

            _collection.DeleteBookFromRepo(bookFolderPath);

            // Simulate a book that was once shared, but has been deleted from the repo folder. But there is no tombstone.
            // (Despite the name, it is only converted to a new local in the default case. When we do a First Time Join,
            // it just gets copied into the repo.)
            MakeBook("Should be converted to new local", "This should become a new local (no status) book as it has local status but is not in the repo", true);
            var delPath = Path.Combine(_repoFolder.FolderPath, "Books", "Should be converted to new local.bloom");

            RobustFile.Delete(delPath);

            // Simulate a book newly created locally. Not in repo, but should not be deleted.
            MakeBook("A book", "This should survive as it has no local status", false);
            // By the way, like most new books, it got renamed early in life...twice
            SimulateRename(_collection, "A book", "An early name");
            SimulateRename(_collection, "An early name", "New book");

            // Simulate a book that needs nothing done to it. It's the same locally and on the repo.
            MakeBook("Keep me", "This needs nothing done to it");

            // Simulate a book that is checked out locally to the current user, but the file has
            // been deleted on the repo.
            MakeBook("Keep me too", "This also needs nothing done", false);
            _collection.WriteLocalStatus("Keep me too", new BookStatus().WithLockedBy("*****@*****.**"));

            // Simlulate a book that is only in the team repo
            MakeBook("Add me", "Fetch to local");
            var delPathAddMe = Path.Combine(_collectionFolder.FolderPath, "Add me");

            SIL.IO.RobustIO.DeleteDirectoryAndContents(delPathAddMe);

            // Simulate a book that was checked in, then checked out again and renamed,
            // but not yet checked in. Both "A renamed book" folder and content and "An old name.bloom"
            // should survive. (Except for an obscure reason when joining a TC...see comment in the test.)
            MakeBook("An old name", "Should be kept in both places with different names");
            _collection.AttemptLock("An old name", "*****@*****.**");
            SimulateRename(_collection, "An old name", "an intermediate name");
            SimulateRename(_collection, "an intermediate name", "A renamed book");

            // Simulate a book that is not checked out locally and has been modified elsewhere
            MakeBook("Update me", "Needs to be become this locally");
            UpdateLocalBook("Update me", "This is supposed to be an older value, not edited locally");

            // Simulate a book that is checked out locally but not in the repo, and where the saved local
            // checksum equals the repo checksum, and it is not checked out in the repo. This would
            // typically indicate that someone remote forced a checkout, perhaps while this user was
            // offline, but checked in again without making changes.
            // Also pretend it has been modified locally.
            // Test result: collection is updated to indicate the local checkout. Local changes are not lost.
            MakeBook("Check me out", "Local and remote checksums correspond to this");
            UpdateLocalBook("Check me out", "This is supposed to be a newer value from local editing", false);
            var oldLocalStatus = _collection.GetLocalStatus("Check me out");
            var newLocalStatus = oldLocalStatus.WithLockedBy(Bloom.TeamCollection.TeamCollectionManager.CurrentUser);

            _checkMeOutOriginalChecksum = oldLocalStatus.checksum;
            _collection.WriteLocalStatus("Check me out", newLocalStatus);

            // Simulate a book that appears newly-created locally (no local status) but is also in the
            // repo. This would indicate two people coincidentally creating a book with the same name.
            // Test result: the local book should get renamed (both folder and htm).
            // When merging while joining a new TC, this case is treated as a conflict and the
            // local book is moved to Lost and Found.
            MakeBook("Rename local", "This content is on the server");
            _collection.AttemptLock("Rename local", "*****@*****.**");
            UpdateLocalBook("Rename local", "This is a new book created independently");
            var statusFilePath = Bloom.TeamCollection.TeamCollection.GetStatusFilePath("Rename local", _collectionFolder.FolderPath);

            RobustFile.Delete(statusFilePath);

            // Simulate a book that is checked out locally but also checked out, to a different user
            // or machine, on the repo. This would indicate some sort of manual intervention, perhaps
            // while this user was long offline. The book has not been modified locally, but the local
            // status is out of date.
            // Test result: local status is updated to reflect the remote checkout, book content updated to repo.
            MakeBook("Update and undo checkout", "This content is everywhere");
            _collection.AttemptLock("Update and undo checkout", "*****@*****.**");
            _collection.WriteLocalStatus("Update and undo checkout", _collection.GetStatus("Update and undo checkout").WithLockedBy(Bloom.TeamCollection.TeamCollectionManager.CurrentUser));

            // Simulate a book that is checked out locally and not on the server, but the repo and (old)
            // local checksums are different. The book has not been edited locally.
            // Test result: book is updated to match repo. Local and remote status should match...review: which wins?
            MakeBook("Update and checkout", "This content is on the server");
            UpdateLocalBook("Update and checkout", "This simulates older content changed remotely but not locally");
            _collection.WriteLocalStatus("Update and checkout", _collection.GetLocalStatus("Update and checkout").WithLockedBy(Bloom.TeamCollection.TeamCollectionManager.CurrentUser));

            // Simulate a book that is checked out and modified locally, but has also been modified
            // remotely.
            // Test result: current local state is saved in lost-and-found. Repo version of book and state
            // copied to local. Warning to user.
            MakeBook("Update content and status and warn", "This simulates new content on server");
            _collection.AttemptLock("Update content and status and warn", "*****@*****.**");
            UpdateLocalBook("Update content and status and warn", "This is supposed to be the newest value from local editing");
            var newStatus = _collection.GetStatus("Update content and status and warn").WithLockedBy(Bloom.TeamCollection.TeamCollectionManager.CurrentUser)
                            .WithChecksum("different from either");

            _collection.WriteLocalStatus("Update content and status and warn", newStatus);

            // Simulate a book that is checked out and modified locally, but is also checked out by another
            // user or machine in the repo. It has not (yet) been modified remotely.
            // Test result: current local state is saved in lost-and-found. Repo version of book and state
            // copied to local. Warning to user.
            MakeBook("Update content and status and warn2", "This simulates new content on server");
            _collection.AttemptLock("Update content and status and warn2", "*****@*****.**");
            UpdateLocalBook("Update content and status and warn2", "This is supposed to be the newest value from local editing", false);
            newStatus = _collection.GetStatus("Update content and status and warn2").WithLockedBy(Bloom.TeamCollection.TeamCollectionManager.CurrentUser);
            _collection.WriteLocalStatus("Update content and status and warn2", newStatus);

            // Simulate a book which has no local status, but for which the computed checksum matches
            // the repo one. This could happen if a user obtained the same book independently,
            // or during initial merging of a local and team collection, where much of the material
            // was previously duplicated.
            // Test result: status is copied to local
            MakeBook("copy status", "Same content in both places");
            _collection.AttemptLock("copy status", "*****@*****.**");
            statusFilePath = Bloom.TeamCollection.TeamCollection.GetStatusFilePath("copy status", _collectionFolder.FolderPath);
            RobustFile.Delete(statusFilePath);

            // Simulate a book that was copied from another TC, using File Explorer.
            // It therefore has a book.status file, but with a different guid.
            // Test result: it should survive, and on a new collection sync get copied into the repo
            var copiedEx = "copied with Explorer";

            MakeBook(copiedEx, "This content is only local", false);
            _collection.WriteLocalStatus(copiedEx, new BookStatus(), collectionId: Bloom.TeamCollection.TeamCollection.GenerateCollectionId());

            // Simulate a book that appeared in DropBox when their software found a conflict.
            // It should NOT be copied locally, but instead moved to Lost and Found, with a report.
            MakeBook(kConflictName, "This content is only on the repo, apart from conflicting copies");
            var conflictFolderPath = Path.Combine(_collectionFolder.FolderPath, kConflictName);

            SIL.IO.RobustIO.DeleteDirectoryAndContents(conflictFolderPath);

            _collection.WriteLocalStatus(copiedEx, new BookStatus(), collectionId: Bloom.TeamCollection.TeamCollection.GenerateCollectionId());

            // Simulate a corrupt zip file, only in the repo
            File.WriteAllText(Path.Combine(_repoFolder.FolderPath, "Books", "new corrupt book.bloom"), "This is not a valid zip!");

            // Simulate a corrupt zip file that corresponds to a local book.
            var badZip = "has a bad zip in repo";

            MakeBook(badZip, "This book seems to be in both places, but the repo is corrupt");
            File.WriteAllText(Path.Combine(_repoFolder.FolderPath, "Books", badZip + ".bloom"), "This is also not a valid zip!");

            // Simulate a book that was renamed remotely. That is, there's a local book Old Name, with local status,
            // and there's no repo book by that name, but there's a repo book New Name (and no such local book).
            // The book's meta.json indicates they are the same book.
            // We'll initially make both, with the new name and new content.
            MakeBook(kNewNameForRemoteRename, "This is the new book content after remote editing and rename");
            var oldFolder = Path.Combine(_collectionFolder.FolderPath, kBookRenamedRemotely);
            var newFolder = Path.Combine(_collectionFolder.FolderPath, kNewNameForRemoteRename);

            RobustIO.MoveDirectory(newFolder, oldFolder);              // made at new path, simulate still at old.
            var oldPath = Path.Combine(_collectionFolder.FolderPath, kBookRenamedRemotely,
                                       kBookRenamedRemotely + ".htm"); // simulate old book name and content

            RobustFile.WriteAllText(oldPath, "This is the simulated original book content");
            RobustFile.Delete(Path.Combine(_collectionFolder.FolderPath, kBookRenamedRemotely,
                                           kNewNameForRemoteRename + ".htm")); // get rid of the 'new' content

            // Simulate a book that is in the repo, where there is a local book that has no status, a different name,
            // and the same ID. This might indicate (a) that it was renamed by someone else after this user's pre-TC
            // copy of the collection diverged; (b) that it was renamed by this user after the divergence;
            // (c) that they were independently copied from some common-ID source.
            // We will treat this as a conflict, moving the local version to lost and found, even on a first time join.
            MakeBook(kRepoNameForIdConflict, "This is the repo version of a book that has a no-status local book with the same ID.");
            // Move the local version to a new folder
            var oldFolder2 = Path.Combine(_collectionFolder.FolderPath, kRepoNameForIdConflict);
            var newFolder2 = Path.Combine(_collectionFolder.FolderPath, kLocalNameForIdConflict);

            RobustIO.MoveDirectory(oldFolder2, newFolder2);
            var localStatusPath =
                Bloom.TeamCollection.TeamCollection.GetStatusFilePath(kLocalNameForIdConflict,
                                                                      _collectionFolder.FolderPath);

            RobustFile.Delete(localStatusPath);

            // Make a couple of folders that are legitimately present, but not books.
            var allowedWords = Path.Combine(_collectionFolder.FolderPath, "Allowed Words");

            Directory.CreateDirectory(allowedWords);
            File.WriteAllText(Path.Combine(allowedWords, "some sample.txt"), "This a fake word list");
            var sampleTexts = Path.Combine(_collectionFolder.FolderPath, "Sample Texts");

            Directory.CreateDirectory(sampleTexts);
            File.WriteAllText(Path.Combine(sampleTexts, "a sample.txt"), "This a fake sample text");

            _progressSpy = new ProgressSpy();

            // sut for the whole suite!
            Assert.That(_collection.SyncAtStartup(_progressSpy, FirstTimeJoin()), Is.True);
        }
        public TeamCollectionManager(string localCollectionPath, BloomWebSocketServer webSocketServer,
                                     BookRenamedEvent bookRenamedEvent, BookStatusChangeEvent bookStatusChangeEvent,
                                     BookSelection bookSelection, CollectionClosing collectionClosingEvent, BookCollectionHolder bookCollectionHolder)
        {
            _webSocketServer       = webSocketServer;
            _bookStatusChangeEvent = bookStatusChangeEvent;
            _localCollectionFolder = Path.GetDirectoryName(localCollectionPath);
            _bookCollectionHolder  = bookCollectionHolder;
            BookSelection          = bookSelection;
            collectionClosingEvent?.Subscribe((x) =>
            {
                // When closing the collection...especially if we're restarting due to
                // changed settings!...we need to save any settings changes to the repo.
                // In such cases we can't safely wait for the change watcher to write things,
                // because (a) if we're shutting down for good, we just might not detect the
                // change before everything shuts down; and (b) if we're reopening the collection,
                // we might overwrite the change with current collection settings before we
                // save the new ones.
                if (CurrentCollection != null)
                {
                    CurrentCollection.SyncLocalAndRepoCollectionFiles(false);
                }
                else if (Settings.HaveEnterpriseFeatures && CurrentCollectionEvenIfDisconnected != null &&
                         CurrentCollectionEvenIfDisconnected is DisconnectedTeamCollection disconnectedTC && disconnectedTC.DisconnectedBecauseNoEnterprise)
                {
                    // We were disconnected because of Enterprise being off, but now the user has
                    // turned Enterprise on again. We really need to save that, even though we usually don't
                    // save settings changes when disconnected. Otherwise, restarting will restore the
                    // no-enterprise state, and we will be stuck.
                    // Note: We don't need to check for admin privileges here. If the user isn't an admin,
                    // he could not have made any changes to settings, including turning on enterprise.
                    var tempCollectionLinkPath = GetTcLinkPathFromLcPath(_localCollectionFolder);
                    if (RobustFile.Exists(tempCollectionLinkPath))
                    {
                        try
                        {
                            var repoFolderPath        = RepoFolderPathFromLinkPath(tempCollectionLinkPath);
                            var tempCollection        = new FolderTeamCollection(this, _localCollectionFolder, repoFolderPath, bookCollectionHolder: _bookCollectionHolder);
                            var problemWithConnection = tempCollection.CheckConnection();
                            if (problemWithConnection == null)
                            {
                                tempCollection.SyncLocalAndRepoCollectionFiles(false);
                            }
                            else
                            {
                                NonFatalProblem.Report(ModalIf.All, PassiveIf.All, "Bloom could not save your settings to the Team Collection: " + problemWithConnection.TextForDisplay,
                                                       null, null, true);
                            }
                        }
                        catch (Exception ex)
                        {
                            NonFatalProblem.Report(ModalIf.All, PassiveIf.All, "Bloom could not save your settings to the Team Collection",
                                                   null, ex, true);
                        }
                    }
                    // What if there's NOT a TC link file? Then it would be pathological to have a CurrentCollectionEvenIfDisconnected.
                    // It's no longer a TC, so we don't need to save the settings to the TC. For now I'm just not going to do anything.
                }
            });
            bookRenamedEvent.Subscribe(pair =>
            {
                CurrentCollectionEvenIfDisconnected?.HandleBookRename(Path.GetFileName(pair.Key), Path.GetFileName(pair.Value));
            });
            var impersonatePath = Path.Combine(_localCollectionFolder, "impersonate.txt");

            if (RobustFile.Exists(impersonatePath))
            {
                var lines = RobustFile.ReadAllLines(impersonatePath);
                _overrideCurrentUser = lines.FirstOrDefault();
                if (lines.Length > 1)
                {
                    _overrideMachineName = lines[1];
                }
                if (lines.Length > 2)
                {
                    _overrideCurrentUserFirstName = lines[2];
                }
                if (lines.Length > 3)
                {
                    _overrideCurrentUserSurname = lines[3];
                }
            }

            var localCollectionLinkPath = GetTcLinkPathFromLcPath(_localCollectionFolder);

            if (RobustFile.Exists(localCollectionLinkPath))
            {
                try
                {
                    var repoFolderPath = RepoFolderPathFromLinkPath(localCollectionLinkPath);
                    CurrentCollection = new FolderTeamCollection(this, _localCollectionFolder, repoFolderPath,
                                                                 bookCollectionHolder: _bookCollectionHolder); // will be replaced if CheckConnection fails
                    // BL-10704: We set this to the CurrentCollection BEFORE checking the connection,
                    // so that there will be a valid MessageLog if we need it during CheckConnection().
                    // If CheckConnection() fails, it will reset this to a DisconnectedTeamCollection.
                    CurrentCollectionEvenIfDisconnected = CurrentCollection;
                    if (CheckConnection())
                    {
                        CurrentCollection.SocketServer = SocketServer;
                        CurrentCollection.TCManager    = this;
                        // Later, we will sync everything else, but we want the current collection settings before
                        // we create the CollectionSettings object.
                        if (ForceNextSyncToLocal)
                        {
                            ForceNextSyncToLocal = false;
                            CurrentCollection.CopyRepoCollectionFilesToLocal(_localCollectionFolder);
                        }
                        else
                        {
                            CurrentCollection.SyncLocalAndRepoCollectionFiles();
                        }
                    }
                    // else CheckConnection has set up a DisconnectedRepo if that is relevant.
                }
                catch (Exception ex)
                {
                    NonFatalProblem.Report(ModalIf.All, PassiveIf.All, "Bloom found Team Collection settings but could not process them", null, ex, true);
                    CurrentCollection = null;
                    CurrentCollectionEvenIfDisconnected = null;
                }
            }
        }
        public void SyncLocalAndRepoCollectionFiles_SyncsInRightDirection()
        {
            using (var collectionFolder =
                       new TemporaryFolder("SyncLocalAndRepoCollectionFiles_SyncsInRightDirection_Collection"))
            {
                using (var repoFolder =
                           new TemporaryFolder("SyncLocalAndRepoCollectionFiles_SyncsInRightDirection_Shared"))
                {
                    var settingsFileName =
                        Path.ChangeExtension(Path.GetFileName(collectionFolder.FolderPath), "bloomCollection");
                    var settingsPath = Path.Combine(collectionFolder.FolderPath, settingsFileName);
                    var tcManager    = new TeamCollectionManager(settingsPath, null, new BookRenamedEvent(),
                                                                 new BookStatusChangeEvent(), null, null);
                    var tc = new FolderTeamCollection(tcManager, collectionFolder.FolderPath, repoFolder.FolderPath);
                    tc.CollectionId = Bloom.TeamCollection.TeamCollection.GenerateCollectionId();
                    var bloomCollectionPath =
                        Bloom.TeamCollection.TeamCollection.CollectionPath(collectionFolder.FolderPath);
                    Assert.That(tc.LocalCollectionFilesRecordedSyncTime, Is.EqualTo(DateTime.MinValue));
                    File.WriteAllText(bloomCollectionPath, "This is a fake collection file");
                    var collectionStylesPath = Path.Combine(collectionFolder.FolderPath, "customCollectionStyles.css");
                    RobustFile.WriteAllText(collectionStylesPath, "This is the collection styles");

                    // SUT 1: nothing in repo, no sync time file. Copies to repo.
                    tc.SyncLocalAndRepoCollectionFiles();

                    var localWriteTime1 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTime1, Is.LessThanOrEqualTo(DateTime.Now));
                    Assert.That(localWriteTime1, Is.GreaterThan(DateTime.Now.Subtract(new TimeSpan(0, 0, 5, 0))));
                    var otherFilesPath = FolderTeamCollection.GetRepoProjectFilesZipPath(repoFolder.FolderPath);
                    Assert.That(File.Exists(otherFilesPath));
                    var anotherPlace = Path.Combine(repoFolder.FolderPath, "anotherPlace.zip");
                    RobustFile.Copy(otherFilesPath, anotherPlace);
                    var repoWriteTime1       = new FileInfo(otherFilesPath).LastWriteTime;
                    var collectionWriteTime1 = new FileInfo(bloomCollectionPath).LastWriteTime;

                    // SUT 2: nothing has changed. But it's a startup, so sync still happens to local.
                    tc.SyncLocalAndRepoCollectionFiles();
                    var localWriteTime2 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTime2, Is.GreaterThanOrEqualTo(localWriteTime1));
                    Assert.That(new FileInfo(otherFilesPath).LastWriteTime, Is.EqualTo(repoWriteTime1));
                    Assert.That(new FileInfo(bloomCollectionPath).LastWriteTime,
                                Is.GreaterThanOrEqualTo(collectionWriteTime1));

                    File.WriteAllText(bloomCollectionPath, "This is a modified fake collection file");
                    var collectionWriteTime2 = new FileInfo(bloomCollectionPath).LastWriteTime;

                    // SUT 3: local change copied to repo (only when not at startup)
                    tc.SyncLocalAndRepoCollectionFiles(false);
                    var localWriteTime3 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTime3, Is.GreaterThan(localWriteTime1),
                                "localWriteTime3 should be greater than localWriteTime1");
                    var repoWriteTime2 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTime2, Is.GreaterThan(repoWriteTime1),
                                "repoWriteTime2 should be greater than repoWriteTime1");
                    // not modified by sync
                    Assert.That(new FileInfo(bloomCollectionPath).LastWriteTime, Is.EqualTo(collectionWriteTime2));

                    File.WriteAllText(bloomCollectionPath, "This is a further modified fake collection file");
                    var collectionWriteTime3 = new FileInfo(bloomCollectionPath).LastWriteTime;
                    var version2Path         = Path.Combine(repoFolder.FolderPath, "version2.zip");
                    RobustFile.Copy(otherFilesPath, version2Path);
                    // modify the remote version by copying the old one back.
                    Thread.Sleep(10);
                    RobustFile.Copy(anotherPlace, otherFilesPath, true);
                    var repoWriteTime3 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTime3, Is.GreaterThan(collectionWriteTime3),
                                "repo file written after local collection file [sanity check]");

                    // SUT 4: both changed: repo wins
                    tc.SyncLocalAndRepoCollectionFiles();
                    var localWriteTime4 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTime4, Is.GreaterThan(localWriteTime3),
                                "localWriteTime4 should be greater than localWriteTime3");
                    var repoWriteTime4 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTime4, Is.EqualTo(repoWriteTime3));                     // not modified by sync
                    Assert.That(new FileInfo(bloomCollectionPath).LastWriteTime, Is.GreaterThan(collectionWriteTime3),
                                "bloomCollection LastWriteTime should be greater than collectionWriteTime3");
                    // We got the original back.
                    Assert.That(File.ReadAllText(bloomCollectionPath), Is.EqualTo("This is a fake collection file"));

                    Thread.Sleep(10);
                    var allowedWords = Path.Combine(collectionFolder.FolderPath, "Allowed Words");
                    Directory.CreateDirectory(allowedWords);
                    File.WriteAllText(Path.Combine(allowedWords, "file1.txt"), "fake word list");

                    // SUT5: local allowed words added
                    tc.SyncLocalAndRepoCollectionFiles(false);
                    var localWriteTime5 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTime5, Is.GreaterThan(localWriteTime4),
                                "localWriteTime5 should be greater than localWriteTime4");
                    var repoWriteTime5 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTime5, Is.GreaterThan(repoWriteTime4),
                                "repoWriteTime5 should be greater than repoWriteTime4");

                    Thread.Sleep(5);
                    var sampleTexts = Path.Combine(collectionFolder.FolderPath, "Sample Texts");
                    Directory.CreateDirectory(sampleTexts);
                    File.WriteAllText(Path.Combine(allowedWords, "sample1.txt"), "fake sample list");

                    // SUT6: local sample texts added
                    tc.SyncLocalAndRepoCollectionFiles(false);
                    var localWriteTime6 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTime6, Is.GreaterThan(localWriteTime5),
                                "localWriteTime6 should be greater than localWriteTime5");
                    var repoWriteTime6 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTime6, Is.GreaterThan(repoWriteTime5),
                                "repoWriteTime6 should be greater than repoWriteTime5");

                    Thread.Sleep(10);
                    File.WriteAllText(Path.Combine(allowedWords, "sample1.txt"), "fake sample list");

                    // SUT7: local file write time modified, but not actually changed. Want the sync time to
                    // update, but NOT to write the remote file.
                    tc.SyncLocalAndRepoCollectionFiles(false);
                    var localWriteTime7 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTime7, Is.GreaterThan(localWriteTime6),
                                "localWriteTime7 should be greater than localWriteTime6");
                    var repoWriteTime7 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTime7, Is.EqualTo(repoWriteTime6));

                    tc._haveShownRemoteSettingsChangeWarning = false;
                    File.WriteAllText(bloomCollectionPath, "This is a modified fake collection file, for SUT 8");
                    var collectionWriteTimeBeforeSut8 = new FileInfo(bloomCollectionPath).LastWriteTime;
                    var localWriteTimeBeforeSut8      = tc.LocalCollectionFilesRecordedSyncTime();
                    var repoWriteTimeBeforeSut8       = new FileInfo(otherFilesPath).LastWriteTime;

                    // SUT 8: local change copied to repo on idle
                    tc.SyncLocalAndRepoCollectionFiles(false);
                    Assert.That(tc._haveShownRemoteSettingsChangeWarning, Is.False, "user should not have been warned");
                    var localWriteTimeAfterSut8 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTimeAfterSut8, Is.GreaterThan(localWriteTimeBeforeSut8),
                                "localWriteTime should increase copying on idle");
                    var repoWriteTimeAfterSut8 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTimeAfterSut8, Is.GreaterThan(repoWriteTimeBeforeSut8),
                                "repoWriteTime should increase copying on idle");
                    // not modified by sync
                    Assert.That(new FileInfo(bloomCollectionPath).LastWriteTime,
                                Is.EqualTo(collectionWriteTimeBeforeSut8));

                    // modify the remote version by copying version2 back.
                    Thread.Sleep(10);
                    var repoWriteTimeBeforeSut9Copy = new FileInfo(otherFilesPath).LastWriteTime;
                    RobustFile.Copy(version2Path, otherFilesPath, true);
                    var collectionWriteTimeBeforeSut9 = new FileInfo(bloomCollectionPath).LastWriteTime;
                    var repoWriteTimeBeforeSut9       = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTimeBeforeSut9, Is.GreaterThan(repoWriteTimeBeforeSut9Copy),
                                "repo file written after local collection file [sanity check]");
                    tc._haveShownRemoteSettingsChangeWarning = false;

                    // SUT9: repo modified, doing check on idle. No changes or warning.
                    tc.SyncLocalAndRepoCollectionFiles(false);
                    Assert.That(tc._haveShownRemoteSettingsChangeWarning, Is.False, "user should not have been warned");
                    var collectionWriteTimeAfterSut9 = new FileInfo(bloomCollectionPath).LastWriteTime;
                    Assert.That(collectionWriteTimeAfterSut9, Is.EqualTo(collectionWriteTimeBeforeSut9),
                                "local settings should not have been modified");

                    File.WriteAllText(bloomCollectionPath, "This is a modified fake collection file, for SUT 10");
                    var collectionWriteTimeBeforeSut10 = new FileInfo(bloomCollectionPath).LastWriteTime;
                    var localWriteTimeBeforeSut10      = tc.LocalCollectionFilesRecordedSyncTime();
                    var repoWriteTimeBeforeSut10       = new FileInfo(otherFilesPath).LastWriteTime;

                    // SUT10: both modified, doing check on idle. No changes. User warned.
                    using (var nfes = new ErrorReport.NonFatalErrorReportExpected())
                    {
                        tc.SyncLocalAndRepoCollectionFiles(false);
                    }

                    Assert.That(tc._haveShownRemoteSettingsChangeWarning, Is.True, "user should have been warned");
                    var localWriteTimeAfterSut10 = tc.LocalCollectionFilesRecordedSyncTime();
                    Assert.That(localWriteTimeAfterSut10, Is.EqualTo(localWriteTimeBeforeSut10),
                                "localWriteTime should not be changed by idle sync where both changed");
                    var repoWriteTimeAfterSut10 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTimeAfterSut10, Is.EqualTo(repoWriteTimeBeforeSut10),
                                "repo should not be modified by idle sync where both changed");                 // not modified by sync
                    Assert.That(new FileInfo(bloomCollectionPath).LastWriteTime,
                                Is.EqualTo(collectionWriteTimeBeforeSut10),
                                "bloomCollection LastWriteTime should not be changed by idle sync both changed");

                    // Get everything back in sync
                    tc.SyncLocalAndRepoCollectionFiles();
                    var localWriteTimeBeforeSut11 = tc.LocalCollectionFilesRecordedSyncTime();
                    var repoWriteTimeBeforeSut11  = new FileInfo(otherFilesPath).LastWriteTime;
                    RobustFile.WriteAllText(collectionStylesPath, "This is the modified collection styles");

                    // SUT11: custom collection styles modified while Bloom was not running. Copied to repo.
                    tc.SyncLocalAndRepoCollectionFiles();
                    var repoWriteTimeAfterSut11 = new FileInfo(otherFilesPath).LastWriteTime;
                    Assert.That(repoWriteTimeAfterSut11, Is.GreaterThanOrEqualTo(repoWriteTimeBeforeSut11));
                    var localWriteTimeAfterSut11 = tc.LocalCollectionFilesRecordedSyncTime();
                    // We will update the sync time even though the write is the other way.
                    Assert.That(localWriteTimeAfterSut11, Is.GreaterThan(localWriteTimeBeforeSut11));
                    Assert.That(File.ReadAllText(collectionStylesPath),
                                Is.EqualTo("This is the modified collection styles"));
                }
            }
        }