diff --git a/src/BitCheck.Tests/ApplicationTests/AddOperationTests.cs b/src/BitCheck.Tests/ApplicationTests/AddOperationTests.cs new file mode 100644 index 0000000..2794617 --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/AddOperationTests.cs @@ -0,0 +1,126 @@ +using BitCheck.Application; +using BitCheck.Database; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class AddOperationTests : ApplicationTestBase + { + [TestMethod] + public void AddDisabled_DoesNotInsertNewEntries() + { + var filePath = Path.Combine(_testDir, "untracked.txt"); + File.WriteAllText(filePath, "data"); + + var options = new AppOptions( + Recursive: false, + Add: false, + Update: true, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(options, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + using var db = new DatabaseService(dbPath); + Assert.IsNull(db.GetFileEntry(Path.GetFileName(filePath)), "File should not be added when --add is false"); + Assert.AreEqual(0, db.GetAllEntries().Count(), "Database should remain empty without add option"); + } + + [TestMethod] + public void AddOnly_SkipsExistingFilesWithoutHashing() + { + var filePath = Path.Combine(_testDir, "existing.txt"); + File.WriteAllText(filePath, "original content"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: true, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + string originalHash; + using (var db = new DatabaseService(dbPath)) + { + var entry = db.GetFileEntry(Path.GetFileName(filePath)); + Assert.IsNotNull(entry, "File should be added initially"); + originalHash = entry.Hash; + } + + using var capture = new StringWriter(); + RunApp(addOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[SKIP]", "Existing file should be skipped on second --add run"); + StringAssert.Contains(output, "Already in database", "Skip message should indicate file is already tracked"); + + using (var db = new DatabaseService(dbPath)) + { + var entry = db.GetFileEntry(Path.GetFileName(filePath)); + Assert.AreEqual(originalHash, entry!.Hash, "Hash should remain unchanged"); + } + } + + [TestMethod] + public void AddWithCheck_DoesNotSkipExistingFiles() + { + var filePath = Path.Combine(_testDir, "checked.txt"); + File.WriteAllText(filePath, "content"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: true, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var addCheckOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: true, + Verbose: true, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(addCheckOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[OK]", "Existing file should be checked when --check is specified"); + Assert.DoesNotContain("Already in database", output, "File should not be skipped when --check is active"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/ApplicationTestBase.cs b/src/BitCheck.Tests/ApplicationTests/ApplicationTestBase.cs new file mode 100644 index 0000000..fdf8155 --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/ApplicationTestBase.cs @@ -0,0 +1,61 @@ +using BitCheck.Application; +using BitCheck.Database; + +namespace BitCheck.Tests.ApplicationTests +{ + /// + /// Base class for BitCheckApplication tests providing common setup, teardown, and helper methods. + /// + public abstract class ApplicationTestBase + { + protected string _testDir = null!; + protected string _originalWorkingDirectory = null!; + + [TestInitialize] + public void Setup() + { + _testDir = Path.Combine(Path.GetTempPath(), $"bitcheck_app_test_{Guid.NewGuid()}"); + Directory.CreateDirectory(_testDir); + _originalWorkingDirectory = Directory.GetCurrentDirectory(); + } + + [TestCleanup] + public void Cleanup() + { + Directory.SetCurrentDirectory(_originalWorkingDirectory); + + if (Directory.Exists(_testDir)) + { + Directory.Delete(_testDir, true); + } + } + + protected static void RunApp(AppOptions options, string workingDirectory, StringWriter? consoleCapture = null) + { + var previous = Directory.GetCurrentDirectory(); + var previousOut = Console.Out; + var previousErr = Console.Error; + Directory.SetCurrentDirectory(workingDirectory); + if (consoleCapture != null) + { + Console.SetOut(consoleCapture); + Console.SetError(consoleCapture); + } + try + { + var app = new BitCheckApplication(options); + app.Run(); + } + finally + { + if (consoleCapture != null) + { + consoleCapture.Flush(); + Console.SetOut(previousOut); + Console.SetError(previousErr); + } + Directory.SetCurrentDirectory(previous); + } + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/CheckOperationTests.cs b/src/BitCheck.Tests/ApplicationTests/CheckOperationTests.cs new file mode 100644 index 0000000..88d9389 --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/CheckOperationTests.cs @@ -0,0 +1,55 @@ +using BitCheck.Application; +using BitCheck.Database; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class CheckOperationTests : ApplicationTestBase + { + [TestMethod] + public void MissingFiles_WithCheckOnly_RetainEntries() + { + var filePath = Path.Combine(_testDir, "orphan.txt"); + File.WriteAllText(filePath, "data"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + File.Delete(filePath); + + var checkOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: true, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(checkOptions, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + using var db = new DatabaseService(dbPath); + var entry = db.GetFileEntry(Path.GetFileName(filePath)); + Assert.IsNotNull(entry, "Entry should remain when update is false"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/DeleteOperationTests.cs b/src/BitCheck.Tests/ApplicationTests/DeleteOperationTests.cs new file mode 100644 index 0000000..e7aa8eb --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/DeleteOperationTests.cs @@ -0,0 +1,128 @@ +using BitCheck.Application; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class DeleteOperationTests : ApplicationTestBase + { + [TestMethod] + public void DeleteWithoutFile_ShowsError() + { + var options = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: null, + Delete: true, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Error:", "Should show error"); + StringAssert.Contains(output, "--delete can only be used with --file", "Error should explain delete requires file"); + } + + [TestMethod] + public void RecursiveWithFile_ShowsError() + { + var filePath = Path.Combine(_testDir, "test.txt"); + File.WriteAllText(filePath, "content"); + + var options = new AppOptions( + Recursive: true, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Error:", "Should show error"); + StringAssert.Contains(output, "--recursive cannot be used with --file", "Error should explain recursive is invalid with file"); + } + + [TestMethod] + public void DeleteWithOtherOperations_ShowsError() + { + var filePath = Path.Combine(_testDir, "test.txt"); + File.WriteAllText(filePath, "content"); + + // Test --delete with --add + var deleteWithAdd = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: true, + Info: false, + List: false); + + using var capture1 = new StringWriter(); + RunApp(deleteWithAdd, _testDir, capture1); + StringAssert.Contains(capture1.ToString(), "--delete cannot be combined with other operations", + "Should reject --delete with --add"); + + // Test --delete with --update + var deleteWithUpdate = new AppOptions( + Recursive: false, + Add: false, + Update: true, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: true, + Info: false, + List: false); + + using var capture2 = new StringWriter(); + RunApp(deleteWithUpdate, _testDir, capture2); + StringAssert.Contains(capture2.ToString(), "--delete cannot be combined with other operations", + "Should reject --delete with --update"); + + // Test --delete with --check + var deleteWithCheck = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: true, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: true, + Info: false, + List: false); + + using var capture3 = new StringWriter(); + RunApp(deleteWithCheck, _testDir, capture3); + StringAssert.Contains(capture3.ToString(), "--delete cannot be combined with other operations", + "Should reject --delete with --check"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/InfoModeTests.cs b/src/BitCheck.Tests/ApplicationTests/InfoModeTests.cs new file mode 100644 index 0000000..d4091da --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/InfoModeTests.cs @@ -0,0 +1,135 @@ +using BitCheck.Application; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class InfoModeTests : ApplicationTestBase + { + [TestMethod] + public void InfoMode_ShowsTrackedFileDetails() + { + var filePath = Path.Combine(_testDir, "infotest.txt"); + File.WriteAllText(filePath, "info test content"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var infoOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: true, + List: false); + + using var capture = new StringWriter(); + RunApp(infoOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[TRACKED]", "Should show file is tracked"); + StringAssert.Contains(output, "Hash:", "Should show hash"); + StringAssert.Contains(output, "Hash Date:", "Should show hash date"); + StringAssert.Contains(output, "Last Check:", "Should show last check date"); + StringAssert.Contains(output, "Current File Status:", "Should show current file status"); + } + + [TestMethod] + public void InfoMode_ShowsNotTrackedForNewFile() + { + var filePath = Path.Combine(_testDir, "untracked.txt"); + File.WriteAllText(filePath, "untracked content"); + + var infoOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: true, + List: false); + + using var capture = new StringWriter(); + RunApp(infoOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[NOT TRACKED]", "Should show file is not tracked"); + } + + [TestMethod] + public void InfoMode_RequiresFileOption() + { + var options = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: null, + Delete: false, + Info: true, + List: false); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Error:", "Should show error"); + StringAssert.Contains(output, "--info can only be used with --file", "Should explain info requires file"); + } + + [TestMethod] + public void InfoMode_CannotBeCombinedWithOtherOperations() + { + var filePath = Path.Combine(_testDir, "test.txt"); + File.WriteAllText(filePath, "content"); + + var options = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: true, + List: false); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Error:", "Should show error"); + StringAssert.Contains(output, "--info cannot be combined with other operations", "Should explain info is standalone"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/ListModeTests.cs b/src/BitCheck.Tests/ApplicationTests/ListModeTests.cs new file mode 100644 index 0000000..a12f2b2 --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/ListModeTests.cs @@ -0,0 +1,154 @@ +using BitCheck.Application; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class ListModeTests : ApplicationTestBase + { + [TestMethod] + public void ListMode_ShowsTrackedFiles() + { + var file1 = Path.Combine(_testDir, "list1.txt"); + var file2 = Path.Combine(_testDir, "list2.txt"); + File.WriteAllText(file1, "content1"); + File.WriteAllText(file2, "content2"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var listOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: true); + + using var capture = new StringWriter(); + RunApp(listOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Mode: List", "Should show list mode"); + StringAssert.Contains(output, "Total files tracked:", "Should show total count"); + StringAssert.Contains(output, "list1.txt", "Should list first file"); + StringAssert.Contains(output, "list2.txt", "Should list second file"); + } + + [TestMethod] + public void ListMode_ShowsMissingFiles() + { + var filePath = Path.Combine(_testDir, "willdelete.txt"); + File.WriteAllText(filePath, "content"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + File.Delete(filePath); + + var listOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: true); + + using var capture = new StringWriter(); + RunApp(listOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[MISSING]", "Should indicate file is missing"); + } + + [TestMethod] + public void ListMode_CannotBeUsedWithFile() + { + var filePath = Path.Combine(_testDir, "test.txt"); + File.WriteAllText(filePath, "content"); + + var options = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: true); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Error:", "Should show error"); + StringAssert.Contains(output, "--list cannot be used with --file", "Should explain list cannot use file"); + } + + [TestMethod] + public void ListMode_CannotBeCombinedWithOtherOperations() + { + var options = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: null, + Delete: false, + Info: false, + List: true); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Error:", "Should show error"); + StringAssert.Contains(output, "--list cannot be combined with other operations", "Should explain list is standalone"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/RecursiveAndDatabaseModeTests.cs b/src/BitCheck.Tests/ApplicationTests/RecursiveAndDatabaseModeTests.cs new file mode 100644 index 0000000..f4343d7 --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/RecursiveAndDatabaseModeTests.cs @@ -0,0 +1,130 @@ +using BitCheck.Application; +using BitCheck.Database; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class RecursiveAndDatabaseModeTests : ApplicationTestBase + { + [TestMethod] + public void RecursiveFalse_SkipsSubdirectories() + { + var subDir = Path.Combine(_testDir, "sub"); + Directory.CreateDirectory(subDir); + + var rootFile = Path.Combine(_testDir, "root.txt"); + var childFile = Path.Combine(subDir, "child.txt"); + File.WriteAllText(rootFile, "root"); + File.WriteAllText(childFile, "child"); + + var options = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(options, _testDir); + + var rootDbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + var childDbPath = Path.Combine(subDir, BitCheckConstants.DatabaseFileName); + + Assert.IsTrue(File.Exists(rootDbPath), "Root directory should have database when processing root files"); + Assert.IsFalse(File.Exists(childDbPath), "Subdirectory should be skipped when recursive=false"); + + using var rootDb = new DatabaseService(rootDbPath); + Assert.IsNotNull(rootDb.GetFileEntry(Path.GetFileName(rootFile)), "Root file should be tracked"); + Assert.IsNull(rootDb.GetFileEntry(Path.GetFileName(childFile)), "Child file should not be tracked when recursion is disabled"); + } + + [TestMethod] + public void SingleDatabaseMode_StoresRelativeKeys() + { + var subDir = Path.Combine(_testDir, "sub"); + Directory.CreateDirectory(subDir); + + var rootFile = Path.Combine(_testDir, "root.txt"); + var childFile = Path.Combine(subDir, "child.txt"); + File.WriteAllText(rootFile, "root"); + File.WriteAllText(childFile, "child"); + + var options = new AppOptions( + Recursive: true, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(options, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + Assert.IsTrue(File.Exists(dbPath), "Single database should exist at root"); + Assert.IsFalse(File.Exists(Path.Combine(subDir, BitCheckConstants.DatabaseFileName)), "Subdirectory should not have its own db in single-db mode"); + + using var db = new DatabaseService(dbPath); + var entries = db.GetAllEntries().ToDictionary(e => e.FileName, e => e); + var expectedChildKey = Path.GetRelativePath(_testDir, childFile); + Assert.IsTrue(entries.ContainsKey(Path.GetRelativePath(_testDir, rootFile)), "Root file should be stored with relative key"); + Assert.IsTrue(entries.ContainsKey(expectedChildKey), "Child file should use relative key"); + } + + [TestMethod] + public void LocalDatabaseMode_CreatesPerDirectoryDatabases() + { + var subDir = Path.Combine(_testDir, "nested"); + Directory.CreateDirectory(subDir); + + var rootFile = Path.Combine(_testDir, "root.txt"); + var childFile = Path.Combine(subDir, "child.txt"); + File.WriteAllText(rootFile, "root"); + File.WriteAllText(childFile, "child"); + + var options = new AppOptions( + Recursive: true, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(options, _testDir); + + var rootDbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + var childDbPath = Path.Combine(subDir, BitCheckConstants.DatabaseFileName); + Assert.IsTrue(File.Exists(rootDbPath), "Root directory should have database"); + Assert.IsTrue(File.Exists(childDbPath), "Sub directory should have its own database"); + + using (var rootDb = new DatabaseService(rootDbPath)) + { + var entry = rootDb.GetFileEntry(Path.GetFileName(rootFile)); + Assert.IsNotNull(entry, "Root file should be stored by name"); + } + + using (var childDb = new DatabaseService(childDbPath)) + { + var entry = childDb.GetFileEntry(Path.GetFileName(childFile)); + Assert.IsNotNull(entry, "Child file should be stored in child db"); + } + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/SingleFileModeTests.cs b/src/BitCheck.Tests/ApplicationTests/SingleFileModeTests.cs new file mode 100644 index 0000000..7fff432 --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/SingleFileModeTests.cs @@ -0,0 +1,376 @@ +using BitCheck.Application; +using BitCheck.Database; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class SingleFileModeTests : ApplicationTestBase + { + [TestMethod] + public void SingleFileMode_Add_AddsFileToDatabase() + { + var filePath = Path.Combine(_testDir, "single.txt"); + File.WriteAllText(filePath, "single file content"); + + var options = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[ADD]", "Single file should be added"); + StringAssert.Contains(output, "Single File:", "Header should indicate single file mode"); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + using var db = new DatabaseService(dbPath); + var entry = db.GetFileEntry(Path.GetFileName(filePath)); + Assert.IsNotNull(entry, "File should be added to database"); + } + + [TestMethod] + public void SingleFileMode_Check_ValidatesExistingFile() + { + var filePath = Path.Combine(_testDir, "checkme.txt"); + File.WriteAllText(filePath, "check content"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var checkOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: true, + Verbose: true, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(checkOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[OK]", "File should pass check"); + StringAssert.Contains(output, "Files checked: 1", "Summary should show 1 file checked"); + } + + [TestMethod] + public void SingleFileMode_Check_DetectsMismatch() + { + var filePath = Path.Combine(_testDir, "mismatch.txt"); + File.WriteAllText(filePath, "original"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var originalModTime = File.GetLastWriteTimeUtc(filePath); + File.WriteAllText(filePath, "modified content"); + File.SetLastWriteTimeUtc(filePath, originalModTime); + + var checkOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: true, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(checkOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[MISMATCH]", "Modified file should be detected as mismatch"); + StringAssert.Contains(output, "Mismatches: 1", "Summary should show 1 mismatch"); + } + + [TestMethod] + public void SingleFileMode_Update_UpdatesHash() + { + var filePath = Path.Combine(_testDir, "updateme.txt"); + File.WriteAllText(filePath, "original"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + string originalHash; + using (var db = new DatabaseService(dbPath)) + { + originalHash = db.GetFileEntry(Path.GetFileName(filePath))!.Hash; + } + + File.WriteAllText(filePath, "modified content that changes hash"); + + var updateOptions = new AppOptions( + Recursive: false, + Add: false, + Update: true, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(updateOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[UPDATE]", "File should be updated"); + + using (var db = new DatabaseService(dbPath)) + { + var entry = db.GetFileEntry(Path.GetFileName(filePath)); + Assert.AreNotEqual(originalHash, entry!.Hash, "Hash should be updated"); + } + } + + [TestMethod] + public void SingleFileMode_Delete_RemovesFromDatabase() + { + var filePath = Path.Combine(_testDir, "deleteme.txt"); + File.WriteAllText(filePath, "to be deleted"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + using (var db = new DatabaseService(dbPath)) + { + Assert.IsNotNull(db.GetFileEntry(Path.GetFileName(filePath)), "File should exist before delete"); + } + + var deleteOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: true, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(deleteOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[DELETED]", "File should be deleted from database"); + StringAssert.Contains(output, "Files removed from database: 1", "Summary should show 1 file removed"); + + using (var db = new DatabaseService(dbPath)) + { + Assert.IsNull(db.GetFileEntry(Path.GetFileName(filePath)), "File should be removed from database"); + } + + Assert.IsTrue(File.Exists(filePath), "Actual file should not be deleted"); + } + + [TestMethod] + public void SingleFileMode_Delete_NotFoundInDatabase() + { + var filePath = Path.Combine(_testDir, "nottracked.txt"); + File.WriteAllText(filePath, "not tracked"); + + var deleteOptions = new AppOptions( + Recursive: false, + Add: false, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: true, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(deleteOptions, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[NOT FOUND]", "Should indicate file not in database"); + StringAssert.Contains(output, "Not in database", "Message should explain file is not tracked"); + } + + [TestMethod] + public void SingleFileMode_WithSingleDb_UsesRelativeKey() + { + var subDir = Path.Combine(_testDir, "subdir"); + Directory.CreateDirectory(subDir); + var filePath = Path.Combine(subDir, "nested.txt"); + File.WriteAllText(filePath, "nested content"); + + var options = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: true, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: filePath, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "[ADD]", "File should be added"); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + using var db = new DatabaseService(dbPath); + + var allEntries = db.GetAllEntries().ToList(); + Assert.HasCount(1, allEntries, "Should have exactly one entry"); + + var storedKey = allEntries[0].FileName; + Assert.DoesNotStartWith(storedKey, "/", $"Key should be relative, not absolute: {storedKey}"); + Assert.DoesNotContain(storedKey, "../", $"Key should not contain parent traversal: {storedKey}"); + Assert.Contains("nested.txt", storedKey, $"Key should contain filename: {storedKey}"); + } + + [TestMethod] + public void SingleFileMode_FileNotFound_ShowsError() + { + var filePath = Path.Combine(_testDir, "nonexistent.txt"); + + var options = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(options, _testDir, capture); + var output = capture.ToString(); + + StringAssert.Contains(output, "Error:", "Should show error for non-existent file"); + StringAssert.Contains(output, "File not found", "Error should indicate file not found"); + } + + [TestMethod] + public void SingleFileMode_Timestamps_TracksTimestamps() + { + var filePath = Path.Combine(_testDir, "timestamped.txt"); + File.WriteAllText(filePath, "timestamp content"); + + var options = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: true, + SingleDatabase: false, + File: filePath, + Delete: false, + Info: false, + List: false); + + RunApp(options, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + using var db = new DatabaseService(dbPath); + var entry = db.GetFileEntry(Path.GetFileName(filePath)); + Assert.IsNotNull(entry, "File should be added"); + + var fileInfo = new FileInfo(filePath); + Assert.AreEqual(fileInfo.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss"), entry.LastModified.ToString("yyyy-MM-dd HH:mm:ss"), "Modified time should be tracked"); + Assert.AreEqual(fileInfo.CreationTimeUtc.ToString("yyyy-MM-dd HH:mm:ss"), entry.CreatedDate.ToString("yyyy-MM-dd HH:mm:ss"), "Created time should be tracked"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/UpdateOperationTests.cs b/src/BitCheck.Tests/ApplicationTests/UpdateOperationTests.cs new file mode 100644 index 0000000..b522962 --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/UpdateOperationTests.cs @@ -0,0 +1,123 @@ +using BitCheck.Application; +using BitCheck.Database; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class UpdateOperationTests : ApplicationTestBase + { + [TestMethod] + public void UpdateWithTimestamps_RefreshesCreationTime() + { + if (!OperatingSystem.IsWindows()) + { + Assert.Inconclusive("Creation time manipulation is only reliable on Windows"); + return; + } + + var filePath = Path.Combine(_testDir, "sample.txt"); + File.WriteAllText(filePath, "content"); + + var appOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: true, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(appOptions, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + DateTime originalCreated; + string originalHash; + using (var db = new DatabaseService(dbPath)) + { + var entry = db.GetFileEntry(Path.GetFileName(filePath))!; + originalCreated = entry.CreatedDate; + originalHash = entry.Hash; + } + + var fileInfo = new FileInfo(filePath); + var newCreatedTime = fileInfo.CreationTimeUtc.AddHours(1); + fileInfo.CreationTimeUtc = newCreatedTime; + + appOptions = new AppOptions( + Recursive: false, + Add: false, + Update: true, + Check: false, + Verbose: false, + Strict: false, + Timestamps: true, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + RunApp(appOptions, _testDir); + + FileEntry updatedEntry; + using (var db = new DatabaseService(dbPath)) + { + updatedEntry = db.GetFileEntry(Path.GetFileName(filePath))!; + } + + Assert.AreNotEqual(originalCreated, newCreatedTime, "Test precondition failed: created time did not change."); + Assert.AreEqual(newCreatedTime, updatedEntry.CreatedDate, "Creation timestamp should be refreshed by update+timestamps."); + Assert.AreEqual(originalHash, updatedEntry.Hash, "Hash should remain aligned with file content."); + } + + [TestMethod] + public void MissingFiles_WithUpdate_RemovedFromDatabase() + { + var filePath = Path.Combine(_testDir, "remove.txt"); + File.WriteAllText(filePath, "data"); + + var addOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(addOptions, _testDir); + + File.Delete(filePath); + + var updateOptions = new AppOptions( + Recursive: false, + Add: false, + Update: true, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + RunApp(updateOptions, _testDir); + + var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); + using var db = new DatabaseService(dbPath); + var entry = db.GetFileEntry(Path.GetFileName(filePath)); + Assert.IsNull(entry, "Entry should be removed when update is true"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/ApplicationTests/VerboseOptionTests.cs b/src/BitCheck.Tests/ApplicationTests/VerboseOptionTests.cs new file mode 100644 index 0000000..67b06ca --- /dev/null +++ b/src/BitCheck.Tests/ApplicationTests/VerboseOptionTests.cs @@ -0,0 +1,60 @@ +using BitCheck.Application; + +namespace BitCheck.Tests.ApplicationTests +{ + [TestClass] + public class VerboseOptionTests : ApplicationTestBase + { + [TestMethod] + public void VerboseOption_WritesProcessingMessages() + { + var filePath = Path.Combine(_testDir, "data.txt"); + File.WriteAllText(filePath, "content"); + + var verboseOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: true, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + using var verboseCapture = new StringWriter(); + RunApp(verboseOptions, _testDir, verboseCapture); + + Assert.Contains("Processing:", verboseCapture.ToString(), "Verbose mode should print processing messages"); + } + + [TestMethod] + public void VerboseDisabled_SuppressesProcessingMessages() + { + var filePath = Path.Combine(_testDir, "data.txt"); + File.WriteAllText(filePath, "content"); + + var quietOptions = new AppOptions( + Recursive: false, + Add: true, + Update: false, + Check: false, + Verbose: false, + Strict: false, + Timestamps: false, + SingleDatabase: true, + File: null, + Delete: false, + Info: false, + List: false); + + using var capture = new StringWriter(); + RunApp(quietOptions, _testDir, capture); + + Assert.DoesNotContain("Processing:", capture.ToString(), "Non-verbose mode should not print processing messages"); + } + } +} \ No newline at end of file diff --git a/src/BitCheck.Tests/BitCheckApplicationTests.cs b/src/BitCheck.Tests/BitCheckApplicationTests.cs deleted file mode 100644 index 25c1af9..0000000 --- a/src/BitCheck.Tests/BitCheckApplicationTests.cs +++ /dev/null @@ -1,1378 +0,0 @@ -using BitCheck.Application; -using BitCheck.Database; -using System.Text; -namespace BitCheck.Tests; - -[TestClass] -public class BitCheckApplicationTests -{ - private string _testDir = null!; - private string _originalWorkingDirectory = null!; - - [TestInitialize] - public void Setup() - { - _testDir = Path.Combine(Path.GetTempPath(), $"bitcheck_app_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(_testDir); - _originalWorkingDirectory = Directory.GetCurrentDirectory(); - } - - [TestCleanup] - public void Cleanup() - { - Directory.SetCurrentDirectory(_originalWorkingDirectory); - - if (Directory.Exists(_testDir)) - { - Directory.Delete(_testDir, true); - } - } - - private static void RunApp(AppOptions options, string workingDirectory, StringWriter? consoleCapture = null) - { - var previous = Directory.GetCurrentDirectory(); - var previousOut = Console.Out; - var previousErr = Console.Error; - Directory.SetCurrentDirectory(workingDirectory); - if (consoleCapture != null) - { - Console.SetOut(consoleCapture); - Console.SetError(consoleCapture); - } - try - { - var app = new BitCheckApplication(options); - app.Run(); - } - finally - { - if (consoleCapture != null) - { - consoleCapture.Flush(); - Console.SetOut(previousOut); - Console.SetError(previousErr); - } - Directory.SetCurrentDirectory(previous); - } - } - - [TestMethod] - public void UpdateWithTimestamps_RefreshesCreationTime() - { - if (!OperatingSystem.IsWindows()) - { - Assert.Inconclusive("Creation time manipulation is only reliable on Windows"); - return; - } - - // Arrange: create initial file - var filePath = Path.Combine(_testDir, "sample.txt"); - File.WriteAllText(filePath, "content"); - - var appOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: true, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(appOptions, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - DateTime originalCreated; - string originalHash; - using (var db = new DatabaseService(dbPath)) - { - var entry = db.GetFileEntry(Path.GetFileName(filePath))!; - originalCreated = entry.CreatedDate; - originalHash = entry.Hash; - } - - // Simulate moving file by touching creation time - var fileInfo = new FileInfo(filePath); - var newCreatedTime = fileInfo.CreationTimeUtc.AddHours(1); - fileInfo.CreationTimeUtc = newCreatedTime; - - // Act: run update+timestamps without check - appOptions = new AppOptions( - Recursive: false, - Add: false, - Update: true, - Check: false, - Verbose: false, - Strict: false, - Timestamps: true, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - RunApp(appOptions, _testDir); - - FileEntry updatedEntry; - using (var db = new DatabaseService(dbPath)) - { - updatedEntry = db.GetFileEntry(Path.GetFileName(filePath))!; - } - - // Assert - Assert.AreNotEqual(originalCreated, newCreatedTime, "Test precondition failed: created time did not change."); - Assert.AreEqual(newCreatedTime, updatedEntry.CreatedDate, "Creation timestamp should be refreshed by update+timestamps."); - Assert.AreEqual(originalHash, updatedEntry.Hash, "Hash should remain aligned with file content."); - } - - [TestMethod] - public void RecursiveFalse_SkipsSubdirectories() - { - var subDir = Path.Combine(_testDir, "sub"); - Directory.CreateDirectory(subDir); - - var rootFile = Path.Combine(_testDir, "root.txt"); - var childFile = Path.Combine(subDir, "child.txt"); - File.WriteAllText(rootFile, "root"); - File.WriteAllText(childFile, "child"); - - var options = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(options, _testDir); - - var rootDbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - var childDbPath = Path.Combine(subDir, BitCheckConstants.DatabaseFileName); - - Assert.IsTrue(File.Exists(rootDbPath), "Root directory should have database when processing root files"); - Assert.IsFalse(File.Exists(childDbPath), "Subdirectory should be skipped when recursive=false"); - - using var rootDb = new DatabaseService(rootDbPath); - Assert.IsNotNull(rootDb.GetFileEntry(Path.GetFileName(rootFile)), "Root file should be tracked"); - Assert.IsNull(rootDb.GetFileEntry(Path.GetFileName(childFile)), "Child file should not be tracked when recursion is disabled"); - } - - [TestMethod] - public void AddDisabled_DoesNotInsertNewEntries() - { - var filePath = Path.Combine(_testDir, "untracked.txt"); - File.WriteAllText(filePath, "data"); - - var options = new AppOptions( - Recursive: false, - Add: false, - Update: true, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(options, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - using var db = new DatabaseService(dbPath); - Assert.IsNull(db.GetFileEntry(Path.GetFileName(filePath)), "File should not be added when --add is false"); - Assert.AreEqual(0, db.GetAllEntries().Count(), "Database should remain empty without add option"); - } - - [TestMethod] - public void VerboseOption_WritesProcessingMessages() - { - var filePath = Path.Combine(_testDir, "data.txt"); - File.WriteAllText(filePath, "content"); - - var verboseOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: true, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - using var verboseCapture = new StringWriter(); - RunApp(verboseOptions, _testDir, verboseCapture); - - StringAssert.Contains(verboseCapture.ToString(), "Processing:", "Verbose mode should print processing messages"); - } - - [TestMethod] - public void VerboseDisabled_SuppressesProcessingMessages() - { - var filePath = Path.Combine(_testDir, "data.txt"); - File.WriteAllText(filePath, "content"); - - var quietOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(quietOptions, _testDir, capture); - - Assert.DoesNotContain("Processing:", capture.ToString(), "Non-verbose mode should not print processing messages"); - } - - private void PrepareFileForStrictCheck(string workingDir) - { - var filePath = Path.Combine(workingDir, "file.txt"); - File.WriteAllText(filePath, "original"); - - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, workingDir); - - var dbPath = Path.Combine(workingDir, BitCheckConstants.DatabaseFileName); - DateTime originalCreated; - using (var db = new DatabaseService(dbPath)) - { - originalCreated = db.GetFileEntry(Path.GetFileName(filePath))!.CreatedDate; - } - - File.WriteAllText(filePath, "modified"); - File.SetCreationTimeUtc(filePath, originalCreated); - } - - private string RunStrictCheck(string workingDir, bool strict) - { - var options = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: true, - Verbose: true, - Strict: strict, - Timestamps: true, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(options, workingDir, capture); - return capture.ToString(); - } - - [TestMethod] - public void SingleDatabaseMode_StoresRelativeKeys() - { - var subDir = Path.Combine(_testDir, "sub"); - Directory.CreateDirectory(subDir); - - var rootFile = Path.Combine(_testDir, "root.txt"); - var childFile = Path.Combine(subDir, "child.txt"); - File.WriteAllText(rootFile, "root"); - File.WriteAllText(childFile, "child"); - - var options = new AppOptions( - Recursive: true, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(options, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - Assert.IsTrue(File.Exists(dbPath), "Single database should exist at root"); - Assert.IsFalse(File.Exists(Path.Combine(subDir, BitCheckConstants.DatabaseFileName)), "Subdirectory should not have its own db in single-db mode"); - - using var db = new DatabaseService(dbPath); - var entries = db.GetAllEntries().ToDictionary(e => e.FileName, e => e); - var expectedChildKey = Path.GetRelativePath(_testDir, childFile); - Assert.IsTrue(entries.ContainsKey(Path.GetRelativePath(_testDir, rootFile)), "Root file should be stored with relative key"); - Assert.IsTrue(entries.ContainsKey(expectedChildKey), "Child file should use relative key"); - } - - [TestMethod] - public void LocalDatabaseMode_CreatesPerDirectoryDatabases() - { - var subDir = Path.Combine(_testDir, "nested"); - Directory.CreateDirectory(subDir); - - var rootFile = Path.Combine(_testDir, "root.txt"); - var childFile = Path.Combine(subDir, "child.txt"); - File.WriteAllText(rootFile, "root"); - File.WriteAllText(childFile, "child"); - - var options = new AppOptions( - Recursive: true, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(options, _testDir); - - var rootDbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - var childDbPath = Path.Combine(subDir, BitCheckConstants.DatabaseFileName); - Assert.IsTrue(File.Exists(rootDbPath), "Root directory should have database"); - Assert.IsTrue(File.Exists(childDbPath), "Sub directory should have its own database"); - - using (var rootDb = new DatabaseService(rootDbPath)) - { - var entry = rootDb.GetFileEntry(Path.GetFileName(rootFile)); - Assert.IsNotNull(entry, "Root file should be stored by name"); - } - - using (var childDb = new DatabaseService(childDbPath)) - { - var entry = childDb.GetFileEntry(Path.GetFileName(childFile)); - Assert.IsNotNull(entry, "Child file should be stored in child db"); - } - } - - [TestMethod] - public void MissingFiles_WithCheckOnly_RetainEntries() - { - var filePath = Path.Combine(_testDir, "orphan.txt"); - File.WriteAllText(filePath, "data"); - - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - File.Delete(filePath); - - var checkOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: true, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(checkOptions, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - using var db = new DatabaseService(dbPath); - var entry = db.GetFileEntry(Path.GetFileName(filePath)); - Assert.IsNotNull(entry, "Entry should remain when update is false"); - } - - [TestMethod] - public void MissingFiles_WithUpdate_RemovedFromDatabase() - { - var filePath = Path.Combine(_testDir, "remove.txt"); - File.WriteAllText(filePath, "data"); - - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - File.Delete(filePath); - - var updateOptions = new AppOptions( - Recursive: false, - Add: false, - Update: true, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(updateOptions, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - using var db = new DatabaseService(dbPath); - var entry = db.GetFileEntry(Path.GetFileName(filePath)); - Assert.IsNull(entry, "Entry should be removed when update is true"); - } - - [TestMethod] - public void AddOnly_SkipsExistingFilesWithoutHashing() - { - // Arrange: create file and add to database - var filePath = Path.Combine(_testDir, "existing.txt"); - File.WriteAllText(filePath, "original content"); - - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: true, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - // Verify file was added - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - string originalHash; - using (var db = new DatabaseService(dbPath)) - { - var entry = db.GetFileEntry(Path.GetFileName(filePath)); - Assert.IsNotNull(entry, "File should be added initially"); - originalHash = entry.Hash; - } - - // Act: run --add again (without --check or --update) - using var capture = new StringWriter(); - RunApp(addOptions, _testDir, capture); - var output = capture.ToString(); - - // Assert: file should be skipped (not re-hashed) - StringAssert.Contains(output, "[SKIP]", "Existing file should be skipped on second --add run"); - StringAssert.Contains(output, "Already in database", "Skip message should indicate file is already tracked"); - - // Verify hash unchanged (proves file wasn't re-processed) - using (var db = new DatabaseService(dbPath)) - { - var entry = db.GetFileEntry(Path.GetFileName(filePath)); - Assert.AreEqual(originalHash, entry!.Hash, "Hash should remain unchanged"); - } - } - - [TestMethod] - public void AddWithCheck_DoesNotSkipExistingFiles() - { - // Arrange: create file and add to database - var filePath = Path.Combine(_testDir, "checked.txt"); - File.WriteAllText(filePath, "content"); - - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: true, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - // Act: run --add --check (should NOT skip existing files) - var addCheckOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: true, - Verbose: true, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(addCheckOptions, _testDir, capture); - var output = capture.ToString(); - - // Assert: file should be checked (OK), not skipped - StringAssert.Contains(output, "[OK]", "Existing file should be checked when --check is specified"); - Assert.DoesNotContain("Already in database", output, "File should not be skipped when --check is active"); - } - - #region Single File Mode Tests - - [TestMethod] - public void SingleFileMode_Add_AddsFileToDatabase() - { - var filePath = Path.Combine(_testDir, "single.txt"); - File.WriteAllText(filePath, "single file content"); - - var options = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[ADD]", "Single file should be added"); - StringAssert.Contains(output, "Single File:", "Header should indicate single file mode"); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - using var db = new DatabaseService(dbPath); - var entry = db.GetFileEntry(Path.GetFileName(filePath)); - Assert.IsNotNull(entry, "File should be added to database"); - } - - [TestMethod] - public void SingleFileMode_Check_ValidatesExistingFile() - { - var filePath = Path.Combine(_testDir, "checkme.txt"); - File.WriteAllText(filePath, "check content"); - - // First add the file - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - // Then check it - var checkOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: true, - Verbose: true, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(checkOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[OK]", "File should pass check"); - StringAssert.Contains(output, "Files checked: 1", "Summary should show 1 file checked"); - } - - [TestMethod] - public void SingleFileMode_Check_DetectsMismatch() - { - var filePath = Path.Combine(_testDir, "mismatch.txt"); - File.WriteAllText(filePath, "original"); - - // Add the file - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - // Modify the file without changing modification time - var originalModTime = File.GetLastWriteTimeUtc(filePath); - File.WriteAllText(filePath, "modified content"); - File.SetLastWriteTimeUtc(filePath, originalModTime); - - // Check it - var checkOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: true, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(checkOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[MISMATCH]", "Modified file should be detected as mismatch"); - StringAssert.Contains(output, "Mismatches: 1", "Summary should show 1 mismatch"); - } - - [TestMethod] - public void SingleFileMode_Update_UpdatesHash() - { - var filePath = Path.Combine(_testDir, "updateme.txt"); - File.WriteAllText(filePath, "original"); - - // Add the file - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - string originalHash; - using (var db = new DatabaseService(dbPath)) - { - originalHash = db.GetFileEntry(Path.GetFileName(filePath))!.Hash; - } - - // Modify the file - File.WriteAllText(filePath, "modified content that changes hash"); - - // Update it - var updateOptions = new AppOptions( - Recursive: false, - Add: false, - Update: true, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(updateOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[UPDATE]", "File should be updated"); - - using (var db = new DatabaseService(dbPath)) - { - var entry = db.GetFileEntry(Path.GetFileName(filePath)); - Assert.AreNotEqual(originalHash, entry!.Hash, "Hash should be updated"); - } - } - - [TestMethod] - public void SingleFileMode_Delete_RemovesFromDatabase() - { - var filePath = Path.Combine(_testDir, "deleteme.txt"); - File.WriteAllText(filePath, "to be deleted"); - - // Add the file first - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - using (var db = new DatabaseService(dbPath)) - { - Assert.IsNotNull(db.GetFileEntry(Path.GetFileName(filePath)), "File should exist before delete"); - } - - // Delete from database - var deleteOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: true, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(deleteOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[DELETED]", "File should be deleted from database"); - StringAssert.Contains(output, "Files removed from database: 1", "Summary should show 1 file removed"); - - using (var db = new DatabaseService(dbPath)) - { - Assert.IsNull(db.GetFileEntry(Path.GetFileName(filePath)), "File should be removed from database"); - } - - // Verify actual file still exists - Assert.IsTrue(File.Exists(filePath), "Actual file should not be deleted"); - } - - [TestMethod] - public void SingleFileMode_Delete_NotFoundInDatabase() - { - var filePath = Path.Combine(_testDir, "nottracked.txt"); - File.WriteAllText(filePath, "not tracked"); - - var deleteOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: true, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(deleteOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[NOT FOUND]", "Should indicate file not in database"); - StringAssert.Contains(output, "Not in database", "Message should explain file is not tracked"); - } - - [TestMethod] - public void SingleFileMode_WithSingleDb_UsesRelativeKey() - { - var subDir = Path.Combine(_testDir, "subdir"); - Directory.CreateDirectory(subDir); - var filePath = Path.Combine(subDir, "nested.txt"); - File.WriteAllText(filePath, "nested content"); - - var options = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: true, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: filePath, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[ADD]", "File should be added"); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - using var db = new DatabaseService(dbPath); - - // Use the same path resolution the application uses to ensure consistency - var rootPath = Path.GetFullPath(_testDir); - var fullFilePath = Path.GetFullPath(filePath); - var expectedKey = Path.GetRelativePath(rootPath, fullFilePath); - - // Diagnostic output for debugging macOS path resolution issues - var allEntries = db.GetAllEntries().ToList(); - var diagnosticInfo = new StringBuilder(); - diagnosticInfo.AppendLine("=== DIAGNOSTIC INFO ==="); - diagnosticInfo.AppendLine($"OS: {Environment.OSVersion}"); - diagnosticInfo.AppendLine($"_testDir (raw): {_testDir}"); - diagnosticInfo.AppendLine($"_testDir (GetFullPath): {rootPath}"); - diagnosticInfo.AppendLine($"filePath (raw): {filePath}"); - diagnosticInfo.AppendLine($"filePath (GetFullPath): {fullFilePath}"); - diagnosticInfo.AppendLine($"expectedKey: {expectedKey}"); - diagnosticInfo.AppendLine($"dbPath: {dbPath}"); - diagnosticInfo.AppendLine($"Database entry count: {allEntries.Count}"); - diagnosticInfo.AppendLine("Database entries:"); - foreach (var e in allEntries) - { - diagnosticInfo.AppendLine($" Key: '{e.FileName}'"); - } - diagnosticInfo.AppendLine($"App output: {output}"); - diagnosticInfo.AppendLine("=== END DIAGNOSTIC INFO ==="); - - var entry = db.GetFileEntry(expectedKey); - Assert.IsNotNull(entry, diagnosticInfo.ToString()); - } - - [TestMethod] - public void SingleFileMode_FileNotFound_ShowsError() - { - var filePath = Path.Combine(_testDir, "nonexistent.txt"); - - var options = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Error:", "Should show error for non-existent file"); - StringAssert.Contains(output, "File not found", "Error should indicate file not found"); - } - - [TestMethod] - public void SingleFileMode_Timestamps_TracksTimestamps() - { - var filePath = Path.Combine(_testDir, "timestamped.txt"); - File.WriteAllText(filePath, "timestamp content"); - - var options = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: true, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - RunApp(options, _testDir); - - var dbPath = Path.Combine(_testDir, BitCheckConstants.DatabaseFileName); - using var db = new DatabaseService(dbPath); - var entry = db.GetFileEntry(Path.GetFileName(filePath)); - Assert.IsNotNull(entry, "File should be added"); - - var fileInfo = new FileInfo(filePath); - Assert.AreEqual(fileInfo.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss"), entry.LastModified.ToString("yyyy-MM-dd HH:mm:ss"), "Modified time should be tracked"); - Assert.AreEqual(fileInfo.CreationTimeUtc.ToString("yyyy-MM-dd HH:mm:ss"), entry.CreatedDate.ToString("yyyy-MM-dd HH:mm:ss"), "Created time should be tracked"); - } - - [TestMethod] - public void DeleteWithoutFile_ShowsError() - { - var options = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: null, - Delete: true, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Error:", "Should show error"); - StringAssert.Contains(output, "--delete can only be used with --file", "Error should explain delete requires file"); - } - - [TestMethod] - public void RecursiveWithFile_ShowsError() - { - var filePath = Path.Combine(_testDir, "test.txt"); - File.WriteAllText(filePath, "content"); - - var options = new AppOptions( - Recursive: true, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Error:", "Should show error"); - StringAssert.Contains(output, "--recursive cannot be used with --file", "Error should explain recursive is invalid with file"); - } - - [TestMethod] - public void DeleteWithOtherOperations_ShowsError() - { - var filePath = Path.Combine(_testDir, "test.txt"); - File.WriteAllText(filePath, "content"); - - // Test --delete with --add - var deleteWithAdd = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: true, - Info: false, - List: false); - - using var capture1 = new StringWriter(); - RunApp(deleteWithAdd, _testDir, capture1); - StringAssert.Contains(capture1.ToString(), "--delete cannot be combined with other operations", - "Should reject --delete with --add"); - - // Test --delete with --update - var deleteWithUpdate = new AppOptions( - Recursive: false, - Add: false, - Update: true, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: true, - Info: false, - List: false); - - using var capture2 = new StringWriter(); - RunApp(deleteWithUpdate, _testDir, capture2); - StringAssert.Contains(capture2.ToString(), "--delete cannot be combined with other operations", - "Should reject --delete with --update"); - - // Test --delete with --check - var deleteWithCheck = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: true, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: true, - Info: false, - List: false); - - using var capture3 = new StringWriter(); - RunApp(deleteWithCheck, _testDir, capture3); - StringAssert.Contains(capture3.ToString(), "--delete cannot be combined with other operations", - "Should reject --delete with --check"); - } - - #endregion - - #region Info and List Mode Tests - - [TestMethod] - public void InfoMode_ShowsTrackedFileDetails() - { - var filePath = Path.Combine(_testDir, "infotest.txt"); - File.WriteAllText(filePath, "info test content"); - - // First add the file - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - // Then get info - var infoOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: true, - List: false); - - using var capture = new StringWriter(); - RunApp(infoOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[TRACKED]", "Should show file is tracked"); - StringAssert.Contains(output, "Hash:", "Should show hash"); - StringAssert.Contains(output, "Hash Date:", "Should show hash date"); - StringAssert.Contains(output, "Last Check:", "Should show last check date"); - StringAssert.Contains(output, "Current File Status:", "Should show current file status"); - } - - [TestMethod] - public void InfoMode_ShowsNotTrackedForNewFile() - { - var filePath = Path.Combine(_testDir, "untracked.txt"); - File.WriteAllText(filePath, "untracked content"); - - var infoOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: true, - List: false); - - using var capture = new StringWriter(); - RunApp(infoOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[NOT TRACKED]", "Should show file is not tracked"); - } - - [TestMethod] - public void InfoMode_RequiresFileOption() - { - var options = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: null, - Delete: false, - Info: true, - List: false); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Error:", "Should show error"); - StringAssert.Contains(output, "--info can only be used with --file", "Should explain info requires file"); - } - - [TestMethod] - public void ListMode_ShowsTrackedFiles() - { - // Create and add files - var file1 = Path.Combine(_testDir, "list1.txt"); - var file2 = Path.Combine(_testDir, "list2.txt"); - File.WriteAllText(file1, "content1"); - File.WriteAllText(file2, "content2"); - - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - // List tracked files - var listOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: true); - - using var capture = new StringWriter(); - RunApp(listOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Mode: List", "Should show list mode"); - StringAssert.Contains(output, "Total files tracked:", "Should show total count"); - StringAssert.Contains(output, "list1.txt", "Should list first file"); - StringAssert.Contains(output, "list2.txt", "Should list second file"); - } - - [TestMethod] - public void ListMode_ShowsMissingFiles() - { - var filePath = Path.Combine(_testDir, "willdelete.txt"); - File.WriteAllText(filePath, "content"); - - var addOptions = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: false); - - RunApp(addOptions, _testDir); - - // Delete the actual file - File.Delete(filePath); - - // List should show it as missing - var listOptions = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: true, - File: null, - Delete: false, - Info: false, - List: true); - - using var capture = new StringWriter(); - RunApp(listOptions, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "[MISSING]", "Should indicate file is missing"); - } - - [TestMethod] - public void ListMode_CannotBeUsedWithFile() - { - var filePath = Path.Combine(_testDir, "test.txt"); - File.WriteAllText(filePath, "content"); - - var options = new AppOptions( - Recursive: false, - Add: false, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: false, - List: true); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Error:", "Should show error"); - StringAssert.Contains(output, "--list cannot be used with --file", "Should explain list cannot use file"); - } - - [TestMethod] - public void ListMode_CannotBeCombinedWithOtherOperations() - { - var options = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: null, - Delete: false, - Info: false, - List: true); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Error:", "Should show error"); - StringAssert.Contains(output, "--list cannot be combined with other operations", "Should explain list is standalone"); - } - - [TestMethod] - public void InfoMode_CannotBeCombinedWithOtherOperations() - { - var filePath = Path.Combine(_testDir, "test.txt"); - File.WriteAllText(filePath, "content"); - - var options = new AppOptions( - Recursive: false, - Add: true, - Update: false, - Check: false, - Verbose: false, - Strict: false, - Timestamps: false, - SingleDatabase: false, - File: filePath, - Delete: false, - Info: true, - List: false); - - using var capture = new StringWriter(); - RunApp(options, _testDir, capture); - var output = capture.ToString(); - - StringAssert.Contains(output, "Error:", "Should show error"); - StringAssert.Contains(output, "--info cannot be combined with other operations", "Should explain info is standalone"); - } - - #endregion -} diff --git a/src/BitCheck.Tests/Usings.cs b/src/BitCheck.Tests/Usings.cs index ab67c7e..3ca376d 100644 --- a/src/BitCheck.Tests/Usings.cs +++ b/src/BitCheck.Tests/Usings.cs @@ -1 +1,3 @@ -global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file +global using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: DoNotParallelize] \ No newline at end of file diff --git a/src/BitCheck/Application/BitCheckApplication.cs b/src/BitCheck/Application/BitCheckApplication.cs index 652d20a..9828dc3 100644 --- a/src/BitCheck/Application/BitCheckApplication.cs +++ b/src/BitCheck/Application/BitCheckApplication.cs @@ -258,7 +258,8 @@ private void WriteSummary(TimeSpan elapsed) /// The path to the file to process. private void ProcessSingleFile(string filePath) { - var fullFilePath = Path.GetFullPath(filePath); + // Resolve the file path - on macOS this resolves symlinks like /var -> /private/var + var fullFilePath = new FileInfo(filePath).FullName; var directory = Path.GetDirectoryName(fullFilePath); var fileName = Path.GetFileName(fullFilePath); @@ -275,7 +276,8 @@ private void ProcessSingleFile(string filePath) if (_options.SingleDatabase) { // In single-db mode, use the database in the current directory with relative path as key - var rootPath = Directory.GetCurrentDirectory(); + // Use DirectoryInfo.FullName to resolve symlinks consistently with FileInfo above + var rootPath = new DirectoryInfo(Directory.GetCurrentDirectory()).FullName; dbPath = Path.Combine(rootPath, BitCheckConstants.DatabaseFileName); databaseKey = Path.GetRelativePath(rootPath, fullFilePath); }