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 HandleModifiedFile_NoConflictBookCheckedOutRemotely_RaisesCheckedOutByOtherButNoNewStuffMessage() { // Setup // // Simulate (sort of) that a book was just overwritten with the following new contents, // including that book.status indicates a remote checkout const string bookFolderName = "My other book"; var bookBuilder = new BookFolderBuilder() .WithRootFolder(_collectionFolder.FolderPath) .WithTitle(bookFolderName) .Build(); string bookFolderPath = bookBuilder.BuiltBookFolderPath; _collection.PutBook(bookFolderPath); _collection.AttemptLock("My other book", "*****@*****.**"); // Enhance: to make it more realistic, we could write a not-checked-out-here local status, // but it's not necessary for producing the effects we want to test here. var prevMessages = _tcLog.Messages.Count; // System Under Test // _collection.HandleModifiedFile(new BookRepoChangeEventArgs() { BookFileName = $"{bookFolderName}.bloom" }); // Verification var eventArgs = (BookStatusChangeEventArgs)_mockTcManager.Invocations.Last().Arguments[0]; Assert.That(eventArgs.CheckedOutByWhom, Is.EqualTo(CheckedOutBy.Other)); Assert.That(_tcLog.Messages.Count, Is.EqualTo(prevMessages)); // checksums didn't change }
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 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); }