diff --git a/README.md b/README.md index ead27cf..a862576 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Ensure that a Markdown file's created date is synchronised with the created-at d ## Usage -Execute FmSync passing a path to a directory which contains files you wish to recursively scan. For any Markdown (*.md) files found, the file's created date will be updated to match that of the `created` date found in the file's Front Matter where one exists. +Execute FmSync passing a path to a directory which contains files you wish to recursively scan. For any Markdown files found, the file's created date will be updated to match that of the `created` date found in the file's Front Matter where one exists. ```powershell fmsync c:\my-markdownfiles @@ -32,3 +32,7 @@ By default, logging is implemented using a single-line simple console logger wit - Alternatively, `TimeZoneId` can be set to any time zone as specified in the `Timezone` column of [this documentation](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones). FmSync will then use this timezone when setting the created date on a file. - If the date given in a file's Front Matter contains a time offset, the TimeZoneId given here will be ignored and the offset given will be taken into account when setting the created date on a file. +### FileSystemOptions + +This contains a single setting, `FilenamePattern`, which by default is `*.md`. Only files matching this filter will be acted upon by FmSync. + diff --git a/src/Elzik.FmSync.Application/Elzik.FmSync.Application.csproj b/src/Elzik.FmSync.Application/Elzik.FmSync.Application.csproj index dcd16c0..5d96248 100644 --- a/src/Elzik.FmSync.Application/Elzik.FmSync.Application.csproj +++ b/src/Elzik.FmSync.Application/Elzik.FmSync.Application.csproj @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -22,6 +23,7 @@ + diff --git a/src/Elzik.FmSync.Application/FrontMatterFolderSynchroniser.cs b/src/Elzik.FmSync.Application/FrontMatterFolderSynchroniser.cs index cd4e409..2398301 100644 --- a/src/Elzik.FmSync.Application/FrontMatterFolderSynchroniser.cs +++ b/src/Elzik.FmSync.Application/FrontMatterFolderSynchroniser.cs @@ -1,5 +1,7 @@ using System.Diagnostics; +using Elzik.FmSync.Infrastructure; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Thinktecture.IO; namespace Elzik.FmSync; @@ -9,22 +11,25 @@ public class FrontMatterFolderSynchroniser : IFrontMatterFolderSynchroniser private readonly ILogger _logger; private readonly IDirectory _directory; private readonly IFrontMatterFileSynchroniser _frontMatterFileSynchroniser; + private readonly FileSystemOptions _options; - public FrontMatterFolderSynchroniser(ILogger logger, - IDirectory directory, IFrontMatterFileSynchroniser frontMatterFileSynchroniser) + public FrontMatterFolderSynchroniser(ILogger logger, IDirectory directory, + IFrontMatterFileSynchroniser frontMatterFileSynchroniser, IOptions options) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _directory = directory ?? throw new ArgumentNullException(nameof(directory)); - _frontMatterFileSynchroniser = frontMatterFileSynchroniser ?? throw new ArgumentNullException(nameof(frontMatterFileSynchroniser)); + _frontMatterFileSynchroniser = frontMatterFileSynchroniser + ?? throw new ArgumentNullException(nameof(frontMatterFileSynchroniser)); + _options = options.Value; } public void SyncCreationDates(string directoryPath) { var loggingInfo = (StartTime: Stopwatch.GetTimestamp(), EditedCount: 0, ErrorCount: 0,TotalCount: 0); - _logger.LogInformation("Synchronising files in {DirectoryPath}", directoryPath); + _logger.LogInformation("Synchronising {FilenamePattern} files in {DirectoryPath}", _options.FilenamePattern, directoryPath); - var markdownFiles = _directory.EnumerateFiles(directoryPath, "*.md", new EnumerationOptions + var markdownFiles = _directory.EnumerateFiles(directoryPath, _options.FilenamePattern, new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = true diff --git a/src/Elzik.FmSync.Console/Presentation/Program.cs b/src/Elzik.FmSync.Console/Presentation/Program.cs index 716a159..05114f9 100644 --- a/src/Elzik.FmSync.Console/Presentation/Program.cs +++ b/src/Elzik.FmSync.Console/Presentation/Program.cs @@ -17,6 +17,7 @@ services.AddTransient(); services.AddTransient(); services.Configure(context.Configuration.GetSection("FrontMatterOptions")); + services.Configure(context.Configuration.GetSection("FileSystemOptions")); services.AddLogging(loggingBuilder => { loggingBuilder.AddConfiguration(context.Configuration.GetSection("Logging")); diff --git a/src/Elzik.FmSync.Console/appSettings.json b/src/Elzik.FmSync.Console/appSettings.json index 53c1127..b6d84e1 100644 --- a/src/Elzik.FmSync.Console/appSettings.json +++ b/src/Elzik.FmSync.Console/appSettings.json @@ -12,5 +12,8 @@ }, "FrontMatterOptions": { "TimeZoneId": "" - } + }, + "FileSystemOptions": { + "FilenamePattern": "*.md" + } } \ No newline at end of file diff --git a/src/Elzik.FmSync.Infrastructure/FileSystemOptions.cs b/src/Elzik.FmSync.Infrastructure/FileSystemOptions.cs new file mode 100644 index 0000000..28e150c --- /dev/null +++ b/src/Elzik.FmSync.Infrastructure/FileSystemOptions.cs @@ -0,0 +1,6 @@ +namespace Elzik.FmSync.Infrastructure; + +public class FileSystemOptions +{ + public string? FilenamePattern { get; set; } +} \ No newline at end of file diff --git a/src/Elzik.FmSync.Infrastructure/FrontMatterOptions.cs b/src/Elzik.FmSync.Infrastructure/FrontMatterOptions.cs index ff2dbdb..5949e6a 100644 --- a/src/Elzik.FmSync.Infrastructure/FrontMatterOptions.cs +++ b/src/Elzik.FmSync.Infrastructure/FrontMatterOptions.cs @@ -1,7 +1,6 @@ -namespace Elzik.FmSync.Infrastructure +namespace Elzik.FmSync.Infrastructure; + +public class FrontMatterOptions { - public class FrontMatterOptions - { - public string? TimeZoneId { get; set; } - } -} + public string? TimeZoneId { get; set; } +} \ No newline at end of file diff --git a/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFileSynchroniserTests.cs b/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFileSynchroniserTests.cs index e8f5a2a..9625792 100644 --- a/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFileSynchroniserTests.cs +++ b/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFileSynchroniserTests.cs @@ -6,134 +6,133 @@ using Thinktecture.IO; using Xunit; -namespace Elzik.FmSync.Application.Tests.Unit +namespace Elzik.FmSync.Application.Tests.Unit; + +public class FrontMatterFileSynchroniserTests { - public class FrontMatterFileSynchroniserTests + private readonly Fixture _fixture; + private readonly MockLogger _mockLogger; + private readonly IMarkdownFrontMatter _mockMarkDownFrontMatter; + private readonly IFile _mockFile; + private readonly FrontMatterFileSynchroniser _frontMatterFileSynchroniser; + + public FrontMatterFileSynchroniserTests() + { + _mockMarkDownFrontMatter = Substitute.For(); + _mockFile = Substitute.For(); + _mockLogger = Substitute.For>(); + + _fixture = new Fixture(); + _fixture.Register>(() => _mockLogger); + _fixture.Register(() => _mockMarkDownFrontMatter); + _fixture.Register(() => _mockFile); + _frontMatterFileSynchroniser = _fixture.Create(); + } + + [Fact] + public void SyncCreationDates_NoMarkDownCreationDate_OnlyLogs() { - private readonly Fixture _fixture; - private readonly MockLogger _mockLogger; - private readonly IMarkdownFrontMatter _mockMarkDownFrontMatter; - private readonly IFile _mockFile; - private readonly FrontMatterFileSynchroniser _frontMatterFileSynchroniser; - - public FrontMatterFileSynchroniserTests() - { - _mockMarkDownFrontMatter = Substitute.For(); - _mockFile = Substitute.For(); - _mockLogger = Substitute.For>(); - - _fixture = new Fixture(); - _fixture.Register>(() => _mockLogger); - _fixture.Register(() => _mockMarkDownFrontMatter); - _fixture.Register(() => _mockFile); - _frontMatterFileSynchroniser = _fixture.Create(); - } - - [Fact] - public void SyncCreationDates_NoMarkDownCreationDate_OnlyLogs() - { - // Arrange - var testFilePath = _fixture.Create(); - _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).ReturnsNull(); - - // Act - _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); - - // Assert - _mockLogger.Received(1).Log(LogLevel.Information, - $"{testFilePath} has no Front Matter created date."); - _mockFile.DidNotReceiveWithAnyArgs().SetCreationTimeUtc(default!, default); - } - - [Fact] - public void SyncCreationDates_MarkDownAndFileDateEqual_OnlyLogs() - { - // Arrange - var testFilePath = _fixture.Create(); - var testDate = _fixture.Create(); - _mockFile.GetCreationTimeUtc(testFilePath).Returns(testDate); - _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).Returns(testDate); - - // Act - _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); - - // Assert - _mockLogger.Received(1).Log( - LogLevel.Information, - Arg.Is>( - dict => - dict.Any(kv => kv.Key == "{OriginalFormat}" - && (string)kv.Value == "{FilePath} has a file created date ({FileCreatedDate}) " + - "the same as the created date specified in its Front Matter.") && - dict.Any(kv => kv.Key == "FilePath" - && (string)kv.Value == testFilePath) && - dict.Any(kv => kv.Key == "FileCreatedDate" - && (DateTime)kv.Value == testDate))); - _mockFile.DidNotReceiveWithAnyArgs().SetCreationTimeUtc(default!, default); - } - - [Fact] - public void SyncCreationDates_FileDateLaterThanMarkdownDate_LogsAndUpdates() - { - // Arrange - var testFilePath = _fixture.Create(); - var testMarkDownDate = _fixture.Create(); - var testFileDate = testMarkDownDate.AddTicks(_fixture.Create()); - _mockFile.GetCreationTimeUtc(testFilePath).Returns(testFileDate); - _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).Returns(testMarkDownDate); - - // Act - _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); - - // Assert - _mockLogger.Received(1).Log( - LogLevel.Information, - Arg.Is>( - dict => - dict.Any(kv => kv.Key == "{OriginalFormat}" - && (string)kv.Value == "{FilePath} has a file created date ({FileCreatedDate}) {RelativeDescription} " + - "than the created date specified in its Front Matter ({FrontMatterCreatedDate})") && - dict.Any(kv => kv.Key == "FilePath" - && (string)kv.Value == testFilePath) && - dict.Any(kv => kv.Key == "FileCreatedDate" - && (DateTime)kv.Value == testFileDate) && - dict.Any(kv => kv.Key == "RelativeDescription" - && (string)kv.Value == "later") && - dict.Any(kv => kv.Key == "FrontMatterCreatedDate" - && (DateTime)kv.Value == testMarkDownDate))); - _mockFile.Received(1).SetCreationTimeUtc(testFilePath, testMarkDownDate); - } - - [Fact] - public void SyncCreationDates_FileDateEarlierThanMarkdownDate_LogsAndUpdates() - { - // Arrange - var testFilePath = _fixture.Create(); - var testMarkDownDate = _fixture.Create(); - var testFileDate = testMarkDownDate.Subtract(_fixture.Create()); - _mockFile.GetCreationTimeUtc(testFilePath).Returns(testFileDate); - _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).Returns(testMarkDownDate); - - // Act - _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); - - // Assert - _mockLogger.Received(1).Log( - LogLevel.Information, - Arg.Is>( - dict => - dict.Any(kv => kv.Key == "{OriginalFormat}" - && (string)kv.Value == "{FilePath} has a file created date ({FileCreatedDate}) {RelativeDescription} " + - "than the created date specified in its Front Matter ({FrontMatterCreatedDate})") && - dict.Any(kv => kv.Key == "FilePath" - && (string)kv.Value == testFilePath) && - dict.Any(kv => kv.Key == "FileCreatedDate" - && (DateTime)kv.Value == testFileDate) && - dict.Any(kv => kv.Key == "RelativeDescription" - && (string)kv.Value == "earlier") && - dict.Any(kv => kv.Key == "FrontMatterCreatedDate" - && (DateTime)kv.Value == testMarkDownDate))); - _mockFile.Received(1).SetCreationTimeUtc(testFilePath, testMarkDownDate); - } + // Arrange + var testFilePath = _fixture.Create(); + _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).ReturnsNull(); + + // Act + _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); + + // Assert + _mockLogger.Received(1).Log(LogLevel.Information, + $"{testFilePath} has no Front Matter created date."); + _mockFile.DidNotReceiveWithAnyArgs().SetCreationTimeUtc(default!, default); + } + + [Fact] + public void SyncCreationDates_MarkDownAndFileDateEqual_OnlyLogs() + { + // Arrange + var testFilePath = _fixture.Create(); + var testDate = _fixture.Create(); + _mockFile.GetCreationTimeUtc(testFilePath).Returns(testDate); + _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).Returns(testDate); + + // Act + _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); + + // Assert + _mockLogger.Received(1).Log( + LogLevel.Information, + Arg.Is>( + dict => + dict.Any(kv => kv.Key == "{OriginalFormat}" + && (string)kv.Value == "{FilePath} has a file created date ({FileCreatedDate}) " + + "the same as the created date specified in its Front Matter.") && + dict.Any(kv => kv.Key == "FilePath" + && (string)kv.Value == testFilePath) && + dict.Any(kv => kv.Key == "FileCreatedDate" + && (DateTime)kv.Value == testDate))); + _mockFile.DidNotReceiveWithAnyArgs().SetCreationTimeUtc(default!, default); + } + + [Fact] + public void SyncCreationDates_FileDateLaterThanMarkdownDate_LogsAndUpdates() + { + // Arrange + var testFilePath = _fixture.Create(); + var testMarkDownDate = _fixture.Create(); + var testFileDate = testMarkDownDate.AddTicks(_fixture.Create()); + _mockFile.GetCreationTimeUtc(testFilePath).Returns(testFileDate); + _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).Returns(testMarkDownDate); + + // Act + _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); + + // Assert + _mockLogger.Received(1).Log( + LogLevel.Information, + Arg.Is>( + dict => + dict.Any(kv => kv.Key == "{OriginalFormat}" + && (string)kv.Value == "{FilePath} has a file created date ({FileCreatedDate}) {RelativeDescription} " + + "than the created date specified in its Front Matter ({FrontMatterCreatedDate})") && + dict.Any(kv => kv.Key == "FilePath" + && (string)kv.Value == testFilePath) && + dict.Any(kv => kv.Key == "FileCreatedDate" + && (DateTime)kv.Value == testFileDate) && + dict.Any(kv => kv.Key == "RelativeDescription" + && (string)kv.Value == "later") && + dict.Any(kv => kv.Key == "FrontMatterCreatedDate" + && (DateTime)kv.Value == testMarkDownDate))); + _mockFile.Received(1).SetCreationTimeUtc(testFilePath, testMarkDownDate); + } + + [Fact] + public void SyncCreationDates_FileDateEarlierThanMarkdownDate_LogsAndUpdates() + { + // Arrange + var testFilePath = _fixture.Create(); + var testMarkDownDate = _fixture.Create(); + var testFileDate = testMarkDownDate.Subtract(_fixture.Create()); + _mockFile.GetCreationTimeUtc(testFilePath).Returns(testFileDate); + _mockMarkDownFrontMatter.GetCreatedDateUtc(testFilePath).Returns(testMarkDownDate); + + // Act + _frontMatterFileSynchroniser.SyncCreationDate(testFilePath); + + // Assert + _mockLogger.Received(1).Log( + LogLevel.Information, + Arg.Is>( + dict => + dict.Any(kv => kv.Key == "{OriginalFormat}" + && (string)kv.Value == "{FilePath} has a file created date ({FileCreatedDate}) {RelativeDescription} " + + "than the created date specified in its Front Matter ({FrontMatterCreatedDate})") && + dict.Any(kv => kv.Key == "FilePath" + && (string)kv.Value == testFilePath) && + dict.Any(kv => kv.Key == "FileCreatedDate" + && (DateTime)kv.Value == testFileDate) && + dict.Any(kv => kv.Key == "RelativeDescription" + && (string)kv.Value == "earlier") && + dict.Any(kv => kv.Key == "FrontMatterCreatedDate" + && (DateTime)kv.Value == testMarkDownDate))); + _mockFile.Received(1).SetCreationTimeUtc(testFilePath, testMarkDownDate); } } \ No newline at end of file diff --git a/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFolderSynchroniserTests.cs b/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFolderSynchroniserTests.cs index efd1bae..ecd3ef5 100644 --- a/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFolderSynchroniserTests.cs +++ b/tests/Elzik.FmSync.Application.Tests.Unit/FrontMatterFolderSynchroniserTests.cs @@ -1,178 +1,186 @@ using AutoFixture; -using Castle.Core.Logging; +using Elzik.FmSync.Infrastructure; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ExceptionExtensions; using Thinktecture.IO; using Xunit; -namespace Elzik.FmSync.Application.Tests.Unit +namespace Elzik.FmSync.Application.Tests.Unit; + +public class FrontMatterFolderSynchroniserTests { - public class FrontMatterFolderSynchroniserTests - { - private readonly Fixture _fixture; - private readonly MockLogger _mockLogger; - private readonly IDirectory _mockDirectory; - private readonly IFrontMatterFileSynchroniser _mockFileSynchroniser; - private readonly FrontMatterFolderSynchroniser _frontMatterFolderSynchroniser; + private readonly Fixture _fixture; + private readonly MockLogger _mockLogger; + private readonly IDirectory _mockDirectory; + private readonly IFrontMatterFileSynchroniser _mockFileSynchroniser; + private readonly FileSystemOptions _testFileSystemOptions; + private readonly FrontMatterFolderSynchroniser _frontMatterFolderSynchroniser; - public FrontMatterFolderSynchroniserTests() - { - _mockLogger = Substitute.For>(); - _mockDirectory = Substitute.For(); - _mockFileSynchroniser = Substitute.For(); - - _fixture = new Fixture(); - _fixture.Register>(() => _mockLogger); - _fixture.Register(() => _mockDirectory); - _fixture.Register(() => _mockFileSynchroniser); - _frontMatterFolderSynchroniser = _fixture.Create(); - } + public FrontMatterFolderSynchroniserTests() + { + _fixture = new Fixture(); - [Fact] - public void SyncCreationDates_DirectoryPathSupplied_OnlyLogs() + _mockLogger = Substitute.For>(); + _mockDirectory = Substitute.For(); + _mockFileSynchroniser = Substitute.For(); + var fileSystemOptions = Options.Create(new FileSystemOptions { - // Arrange - var testDirectoryPath = _fixture.Create(); + FilenamePattern = _fixture.Create() + }); + _testFileSystemOptions = fileSystemOptions.Value; + _fixture.Register>(() => _mockLogger); + _fixture.Register(() => _mockDirectory); + _fixture.Register(() => _mockFileSynchroniser); + _fixture.Register(() => fileSystemOptions); + + _frontMatterFolderSynchroniser = _fixture.Create(); + } + + [Fact] + public void SyncCreationDates_DirectoryPathSupplied_OnlyLogs() + { + // Arrange + var testDirectoryPath = _fixture.Create(); - // Act - _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); + // Act + _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); - // Assert - _mockLogger.Received(1).Log(LogLevel.Information, $"Synchronising files in {testDirectoryPath}"); + // Assert + _mockLogger.Received(1).Log(LogLevel.Information, $"Synchronising {_testFileSystemOptions.FilenamePattern} files in {testDirectoryPath}"); - _mockFileSynchroniser.DidNotReceiveWithAnyArgs().SyncCreationDate(default!); - } + _mockFileSynchroniser.DidNotReceiveWithAnyArgs().SyncCreationDate(default!); + } - [Fact] - public void SyncCreationDates_NoMarkDownFiles_OnlyLogs() - { - // Arrange - var testDirectoryPath = _fixture.Create(); + [Fact] + public void SyncCreationDates_NoMarkDownFiles_OnlyLogs() + { + // Arrange + var testDirectoryPath = _fixture.Create(); - // Act - _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); + // Act + _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); - // Assert - _mockLogger.Received(1).Log(LogLevel.Information, Arg.Is(s => - s.StartsWith("Synchronised 0 files out of a total 0 in"))); - _mockFileSynchroniser.DidNotReceiveWithAnyArgs().SyncCreationDate(default!); - } + // Assert + _mockLogger.Received(1).Log(LogLevel.Information, Arg.Is(s => + s.StartsWith("Synchronised 0 files out of a total 0 in"))); + _mockFileSynchroniser.DidNotReceiveWithAnyArgs().SyncCreationDate(default!); + } - [Fact] - public void SyncCreationDates_WithMarkDownFiles_SyncsThoseFiles() - { - // Arrange - var testDirectoryPath = _fixture.Create(); - var testFiles = _fixture.CreateMany>().ToList(); - SetMockDirectoryFilePaths(testDirectoryPath, testFiles); + [Fact] + public void SyncCreationDates_WithMarkDownFiles_SyncsThoseFiles() + { + // Arrange + var testDirectoryPath = _fixture.Create(); + var testFiles = _fixture.CreateMany>().ToList(); + SetMockDirectoryFilePaths(testDirectoryPath, testFiles); - // Act - _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); + // Act + _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); - // Assert - _mockFileSynchroniser.ReceivedWithAnyArgs(testFiles.Count).SyncCreationDate(default!); - foreach (var testFilesPath in testFiles) - { - _mockFileSynchroniser.Received(1).SyncCreationDate(testFilesPath.Key); - } + // Assert + _mockFileSynchroniser.ReceivedWithAnyArgs(testFiles.Count).SyncCreationDate(default!); + foreach (var testFilesPath in testFiles) + { + _mockFileSynchroniser.Received(1).SyncCreationDate(testFilesPath.Key); } + } - [Fact] - public void SyncCreationDates_WithMarkDownFiles_LogsSummary() - { - // Arrange - var testDirectoryPath = _fixture.Create(); - var testFiles = _fixture.CreateMany>().ToList(); - SetMockDirectoryFilePaths(testDirectoryPath, testFiles); + [Fact] + public void SyncCreationDates_WithMarkDownFiles_LogsSummary() + { + // Arrange + var testDirectoryPath = _fixture.Create(); + var testFiles = _fixture.CreateMany>().ToList(); + SetMockDirectoryFilePaths(testDirectoryPath, testFiles); - // Act - _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); + // Act + _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); - // Assert - _mockLogger.Received(1).Log(LogLevel.Information, Arg.Is(s => - s.StartsWith($"Synchronised {testFiles.Count(pair => pair.Value)} files out of a total {testFiles.Count} in"))); - } + // Assert + _mockLogger.Received(1).Log(LogLevel.Information, Arg.Is(s => + s.StartsWith($"Synchronised {testFiles.Count(pair => pair.Value)} files out of a total {testFiles.Count} in"))); + } - [Fact] - public void SyncCreationDates_SyncFailsWithInnerException_LogsError() - { - // Arrange - var testDirectoryPath = _fixture.Create(); - var testFile = _fixture.Create>(); - var testFiles = new List> { testFile }; - SetMockDirectoryFilePaths(testDirectoryPath, testFiles); - var testException = new Exception(_fixture.Create(), _fixture.Create()); - _mockFileSynchroniser.SyncCreationDate(testFile.Key).Throws(testException); - - // Act - _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); - - // Assert - _mockLogger.Received(1).Log( LogLevel.Error, - testFile.Key + " - " + testException.Message + " " + testException.InnerException?.Message); - } + [Fact] + public void SyncCreationDates_SyncFailsWithInnerException_LogsError() + { + // Arrange + var testDirectoryPath = _fixture.Create(); + var testFile = _fixture.Create>(); + var testFiles = new List> { testFile }; + SetMockDirectoryFilePaths(testDirectoryPath, testFiles); + var testException = new Exception(_fixture.Create(), _fixture.Create()); + _mockFileSynchroniser.SyncCreationDate(testFile.Key).Throws(testException); + + // Act + _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); + + // Assert + _mockLogger.Received(1).Log( LogLevel.Error, + testFile.Key + " - " + testException.Message + " " + testException.InnerException?.Message); + } - [Fact] - public void SyncCreationDates_SyncFailsWithoutInnerException_LogsError() - { - // Arrange - var testDirectoryPath = _fixture.Create(); - var testFile = _fixture.Create>(); - var testFiles = new List> { testFile }; - SetMockDirectoryFilePaths(testDirectoryPath, testFiles); - var testException = new Exception(_fixture.Create()); - _mockFileSynchroniser.SyncCreationDate(testFile.Key).Throws(testException); - - // Act - _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); - - // Assert - _mockLogger.Received(1).Log(LogLevel.Error, - testFile.Key + " - " + testException.Message); - } + [Fact] + public void SyncCreationDates_SyncFailsWithoutInnerException_LogsError() + { + // Arrange + var testDirectoryPath = _fixture.Create(); + var testFile = _fixture.Create>(); + var testFiles = new List> { testFile }; + SetMockDirectoryFilePaths(testDirectoryPath, testFiles); + var testException = new Exception(_fixture.Create()); + _mockFileSynchroniser.SyncCreationDate(testFile.Key).Throws(testException); + + // Act + _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); + + // Assert + _mockLogger.Received(1).Log(LogLevel.Error, + testFile.Key + " - " + testException.Message); + } - [Fact] - public void SyncCreationDates_SyncFails_LogsSummary() + [Fact] + public void SyncCreationDates_SyncFails_LogsSummary() + { + // Arrange + var testDirectoryPath = _fixture.Create(); + var testFailingFile = new KeyValuePair(_fixture.Create(), false); + var testFiles = new [] { - // Arrange - var testDirectoryPath = _fixture.Create(); - var testFailingFile = new KeyValuePair(_fixture.Create(), false); - var testFiles = new [] - { - testFailingFile, - new (_fixture.Create(), false), - new (_fixture.Create(), true) - }; - SetMockDirectoryFilePaths(testDirectoryPath, testFiles); - var testException = new Exception(_fixture.Create(), _fixture.Create()); - _mockFileSynchroniser.SyncCreationDate(testFailingFile.Key).Throws(testException); - - // Act - _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); - - // Assert - _mockLogger.Received(1).Log(LogLevel.Information, Arg.Is(s => - s.StartsWith("Synchronised 1 and failed 1 files out of a total 3 in"))); - } + testFailingFile, + new (_fixture.Create(), false), + new (_fixture.Create(), true) + }; + SetMockDirectoryFilePaths(testDirectoryPath, testFiles); + var testException = new Exception(_fixture.Create(), _fixture.Create()); + _mockFileSynchroniser.SyncCreationDate(testFailingFile.Key).Throws(testException); + + // Act + _frontMatterFolderSynchroniser.SyncCreationDates(testDirectoryPath); + + // Assert + _mockLogger.Received(1).Log(LogLevel.Information, Arg.Is(s => + s.StartsWith("Synchronised 1 and failed 1 files out of a total 3 in"))); + } - private void SetMockDirectoryFilePaths(string testDirectoryPath, IEnumerable> testFiles) - { - var testFileList = testFiles.ToList(); + private void SetMockDirectoryFilePaths(string testDirectoryPath, IEnumerable> testFiles) + { + var testFileList = testFiles.ToList(); - _mockDirectory.EnumerateFiles(testDirectoryPath, "*.md", - Arg.Is(options => - options.MatchCasing == MatchCasing.CaseInsensitive && options.RecurseSubdirectories)) - .Returns(testFileList.Select(pair => pair.Key)); + _mockDirectory.EnumerateFiles(testDirectoryPath, _testFileSystemOptions.FilenamePattern!, + Arg.Is(options => + options.MatchCasing == MatchCasing.CaseInsensitive && options.RecurseSubdirectories)) + .Returns(testFileList.Select(pair => pair.Key)); - foreach (var testFilePath in testFileList) + foreach (var testFilePath in testFileList) + { + _mockFileSynchroniser.SyncCreationDate(testFilePath.Key).Returns(new SyncResult { - _mockFileSynchroniser.SyncCreationDate(testFilePath.Key).Returns(new SyncResult - { - FileCreatedDateUpdated = testFilePath.Value - }); - } + FileCreatedDateUpdated = testFilePath.Value + }); } } } \ No newline at end of file diff --git a/tests/Elzik.FmSync.Application.Tests.Unit/MockLogger.cs b/tests/Elzik.FmSync.Application.Tests.Unit/MockLogger.cs index 6e363ad..701e713 100644 --- a/tests/Elzik.FmSync.Application.Tests.Unit/MockLogger.cs +++ b/tests/Elzik.FmSync.Application.Tests.Unit/MockLogger.cs @@ -1,39 +1,38 @@ -namespace Elzik.FmSync.Application.Tests.Unit -{ - using Microsoft.Extensions.Logging; +namespace Elzik.FmSync.Application.Tests.Unit; - // This class is necessary because mocking an ILogger will not work; further explanation here: - // https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-1081422618 +using Microsoft.Extensions.Logging; - public abstract class MockLogger : ILogger - { - public void Log(LogLevel logLevel, EventId eventId, TState state, - Exception? exception, Func formatter) - { - var unboxed = (IReadOnlyList>)state!; - var message = formatter(state, exception); +// This class is necessary because mocking an ILogger will not work; further explanation here: +// https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-1081422618 - Log(); - Log(logLevel, message); - Log(logLevel, message, exception); - Log(logLevel, unboxed.ToDictionary(k => - k.Key, v => v.Value)); - Log(logLevel, unboxed.ToDictionary(k => - k.Key, v => v.Value), exception); - } +public abstract class MockLogger : ILogger +{ + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception? exception, Func formatter) + { + var unboxed = (IReadOnlyList>)state!; + var message = formatter(state, exception); + + Log(); + Log(logLevel, message); + Log(logLevel, message, exception); + Log(logLevel, unboxed.ToDictionary(k => + k.Key, v => v.Value)); + Log(logLevel, unboxed.ToDictionary(k => + k.Key, v => v.Value), exception); + } - public abstract void Log(); + public abstract void Log(); - public abstract void Log(LogLevel logLevel, string message); + public abstract void Log(LogLevel logLevel, string message); - public abstract void Log(LogLevel logLevel, string message, Exception? exception); + public abstract void Log(LogLevel logLevel, string message, Exception? exception); - public abstract void Log(LogLevel logLevel, IDictionary state); + public abstract void Log(LogLevel logLevel, IDictionary state); - public abstract void Log(LogLevel logLevel, IDictionary state, Exception? exception); + public abstract void Log(LogLevel logLevel, IDictionary state, Exception? exception); - public virtual bool IsEnabled(LogLevel logLevel) => true; + public virtual bool IsEnabled(LogLevel logLevel) => true; - public abstract IDisposable? BeginScope(TState state) where TState : notnull; - } -} + public abstract IDisposable? BeginScope(TState state) where TState : notnull; +} \ No newline at end of file diff --git a/tests/Elzik.FmSync.Infrastructure.Tests.Integration/MarkdownFrontMatterTests.cs b/tests/Elzik.FmSync.Infrastructure.Tests.Integration/MarkdownFrontMatterTests.cs index 4ce1c3f..37c50f2 100644 --- a/tests/Elzik.FmSync.Infrastructure.Tests.Integration/MarkdownFrontMatterTests.cs +++ b/tests/Elzik.FmSync.Infrastructure.Tests.Integration/MarkdownFrontMatterTests.cs @@ -5,129 +5,128 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Elzik.FmSync.Infrastructure.Tests.Integration +namespace Elzik.FmSync.Infrastructure.Tests.Integration; + +public sealed class MarkdownFrontMatterTests : IDisposable { - public sealed class MarkdownFrontMatterTests : IDisposable + [Theory] + [InlineData("./TestFiles/YamlContainsOnlyCreatedDate.md", "GMT Standard Time", null, "2023-01-07 14:28:22", + "because the current timezone is GMT")] + [InlineData("./TestFiles/YamlContainsOnlyCreatedDateWithPrecedingNewLines.md", "GMT Standard Time", null, "2023-01-07 14:28:22", + "because the current timezone is GMT")] + [InlineData("./TestFiles/YamlContainsCreatedDateAndOtherValue.md", "GMT Standard Time", null, "2022-03-31 13:52:15", + "because the current timezone is GMT at BST")] + [InlineData("./TestFiles/YamlContainsOnlyCreatedDate.md", "AUS Eastern Standard Time", null, "2023-01-07 03:28:22", + "because the current timezone is AUS")] + [InlineData("./TestFiles/YamlContainsCreatedDateAndOtherValue.md", "AUS Eastern Standard Time", null, "2022-03-31 03:52:15", + "because the current timezone is AUS")] + [InlineData("./TestFiles/YamlContainsOnlyCreatedDateWithPrecedingNewLines.md", "AUS Eastern Standard Time", null, "2023-01-07 03:28:22", + "because the current timezone is AUS")] + [InlineData("./TestFiles/YamlContainsOnlyCreatedDate.md", "GMT Standard Time", "AUS Eastern Standard Time", "2023-01-07 03:28:22", + "because even though the current timezone is GMT the YAML is configured for AUS")] + [InlineData("./TestFiles/YamlContainsCreatedDateAndOtherValue.md", "GMT Standard Time", "AUS Eastern Standard Time", "2022-03-31 03:52:15", + "because even though the current timezone is GMT the YAML is configured for AUS")] + [InlineData("./TestFiles/YamlContainsOnlyCreatedDateWithPrecedingNewLines.md", "GMT Standard Time", "AUS Eastern Standard Time", "2023-01-07 03:28:22", + "because even though the current timezone is GMT the YAML is configured for AUS")] + [InlineData("./TestFiles/YamlContainsOnlyDateWithOffset.md", "GMT Standard Time", null, "2023-02-14 13:20:32", + "because even though our current timezone is GMT it is ignored because a time offset was supplied in the Front Matter data")] + [InlineData("./TestFiles/YamlContainsOnlyDateWithOffset.md", "GMT Standard Time", "AUS Eastern Standard Time", "2023-02-14 13:20:32", + "because even though our current timezone is GMT and the Front Matter is configured for AUS they are both ignored because a time offset was supplied in the Front Matter data")] + public void GetCreatedDate_YamlContainsCreatedDate_ReturnsCreatedDate(string testFilePath, string localTimeZone, + string configuredTimeZone, string expectedUtcDateString, string because) { - [Theory] - [InlineData("./TestFiles/YamlContainsOnlyCreatedDate.md", "GMT Standard Time", null, "2023-01-07 14:28:22", - "because the current timezone is GMT")] - [InlineData("./TestFiles/YamlContainsOnlyCreatedDateWithPrecedingNewLines.md", "GMT Standard Time", null, "2023-01-07 14:28:22", - "because the current timezone is GMT")] - [InlineData("./TestFiles/YamlContainsCreatedDateAndOtherValue.md", "GMT Standard Time", null, "2022-03-31 13:52:15", - "because the current timezone is GMT at BST")] - [InlineData("./TestFiles/YamlContainsOnlyCreatedDate.md", "AUS Eastern Standard Time", null, "2023-01-07 03:28:22", - "because the current timezone is AUS")] - [InlineData("./TestFiles/YamlContainsCreatedDateAndOtherValue.md", "AUS Eastern Standard Time", null, "2022-03-31 03:52:15", - "because the current timezone is AUS")] - [InlineData("./TestFiles/YamlContainsOnlyCreatedDateWithPrecedingNewLines.md", "AUS Eastern Standard Time", null, "2023-01-07 03:28:22", - "because the current timezone is AUS")] - [InlineData("./TestFiles/YamlContainsOnlyCreatedDate.md", "GMT Standard Time", "AUS Eastern Standard Time", "2023-01-07 03:28:22", - "because even though the current timezone is GMT the YAML is configured for AUS")] - [InlineData("./TestFiles/YamlContainsCreatedDateAndOtherValue.md", "GMT Standard Time", "AUS Eastern Standard Time", "2022-03-31 03:52:15", - "because even though the current timezone is GMT the YAML is configured for AUS")] - [InlineData("./TestFiles/YamlContainsOnlyCreatedDateWithPrecedingNewLines.md", "GMT Standard Time", "AUS Eastern Standard Time", "2023-01-07 03:28:22", - "because even though the current timezone is GMT the YAML is configured for AUS")] - [InlineData("./TestFiles/YamlContainsOnlyDateWithOffset.md", "GMT Standard Time", null, "2023-02-14 13:20:32", - "because even though our current timezone is GMT it is ignored because a time offset was supplied in the Front Matter data")] - [InlineData("./TestFiles/YamlContainsOnlyDateWithOffset.md", "GMT Standard Time", "AUS Eastern Standard Time", "2023-02-14 13:20:32", - "because even though our current timezone is GMT and the Front Matter is configured for AUS they are both ignored because a time offset was supplied in the Front Matter data")] - public void GetCreatedDate_YamlContainsCreatedDate_ReturnsCreatedDate(string testFilePath, string localTimeZone, - string configuredTimeZone, string expectedUtcDateString, string because) - { - // Arrange - SetTimeZone(localTimeZone); - var testOptions = Options.Create(new FrontMatterOptions - { - TimeZoneId = configuredTimeZone - }); - var expectedDateUtc = DateTime.ParseExact(expectedUtcDateString, "yyyy-MM-dd HH:mm:ss", - CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); - - // Act - var markdownFrontMatter = new MarkdownFrontMatter(testOptions); - var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); - - // Assert - createdDate.Should().Be(expectedDateUtc, because); - createdDate!.Value.Kind.Should().Be(DateTimeKind.Utc); - } - - [Theory] - [InlineData("./TestFiles/YamlIsEmpty.md")] - [InlineData("./TestFiles/YamlContainsOnlyWhitespace.md")] - [InlineData("./TestFiles/YamlContainsOnlyNonCreatedDate.md")] - [InlineData("./TestFiles/YamlIsMissingBodyIsPresent.md")] - [InlineData("./TestFiles/YamlSectionNeverClosed.md")] - [InlineData("./TestFiles/YamlSectionNeverOpened.md")] - [InlineData("./TestFiles/TextPrecedesYamlSection.md")] - [InlineData("./TestFiles/WhitespacePrecedesYamlSection.md")] - [InlineData("./TestFiles/YamlSectionOpenedWithExtraCharacters.md")] - [InlineData("./TestFiles/YamlSectionClosedWithExtraCharacters.md")] - public void GetCreatedDate_YamlContainsNoCreatedDateOrNoValidYaml_ReturnsNullDate(string testFilePath) + // Arrange + SetTimeZone(localTimeZone); + var testOptions = Options.Create(new FrontMatterOptions { - // Arrange - var testOptions = Options.Create(new FrontMatterOptions()); + TimeZoneId = configuredTimeZone + }); + var expectedDateUtc = DateTime.ParseExact(expectedUtcDateString, "yyyy-MM-dd HH:mm:ss", + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + + // Act + var markdownFrontMatter = new MarkdownFrontMatter(testOptions); + var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); + + // Assert + createdDate.Should().Be(expectedDateUtc, because); + createdDate!.Value.Kind.Should().Be(DateTimeKind.Utc); + } - // Act - var markdownFrontMatter = new MarkdownFrontMatter(testOptions); - var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); + [Theory] + [InlineData("./TestFiles/YamlIsEmpty.md")] + [InlineData("./TestFiles/YamlContainsOnlyWhitespace.md")] + [InlineData("./TestFiles/YamlContainsOnlyNonCreatedDate.md")] + [InlineData("./TestFiles/YamlIsMissingBodyIsPresent.md")] + [InlineData("./TestFiles/YamlSectionNeverClosed.md")] + [InlineData("./TestFiles/YamlSectionNeverOpened.md")] + [InlineData("./TestFiles/TextPrecedesYamlSection.md")] + [InlineData("./TestFiles/WhitespacePrecedesYamlSection.md")] + [InlineData("./TestFiles/YamlSectionOpenedWithExtraCharacters.md")] + [InlineData("./TestFiles/YamlSectionClosedWithExtraCharacters.md")] + public void GetCreatedDate_YamlContainsNoCreatedDateOrNoValidYaml_ReturnsNullDate(string testFilePath) + { + // Arrange + var testOptions = Options.Create(new FrontMatterOptions()); - // Assert - createdDate.Should().BeNull(); - } + // Act + var markdownFrontMatter = new MarkdownFrontMatter(testOptions); + var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); - [Theory] - [InlineData("./TestFiles/YamlContainsOnlyMinCreatedDate.md")] - public void GetCreatedDate_YamlContainsMinDate_ReturnsCreatedDate(string testFilePath) - { - // Arrange - var testOptions = Options.Create(new FrontMatterOptions()); - var expectedDateUtc = DateTime.MinValue; + // Assert + createdDate.Should().BeNull(); + } - // Act - var markdownFrontMatter = new MarkdownFrontMatter(testOptions); - var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); + [Theory] + [InlineData("./TestFiles/YamlContainsOnlyMinCreatedDate.md")] + public void GetCreatedDate_YamlContainsMinDate_ReturnsCreatedDate(string testFilePath) + { + // Arrange + var testOptions = Options.Create(new FrontMatterOptions()); + var expectedDateUtc = DateTime.MinValue; - // Assert - createdDate.Should().Be(expectedDateUtc); - } + // Act + var markdownFrontMatter = new MarkdownFrontMatter(testOptions); + var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); - [Theory] - [InlineData("./TestFiles/YamlContainsOnlyMaxCreatedDate.md")] - public void GetCreatedDate_YamlContainsMaxDate_ReturnsCreatedDate(string testFilePath) - { - // Arrange - var testOptions = Options.Create(new FrontMatterOptions()); - var expectedDateUtc = DateTime.MaxValue; + // Assert + createdDate.Should().Be(expectedDateUtc); + } - // Act - var markdownFrontMatter = new MarkdownFrontMatter(testOptions); - var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); + [Theory] + [InlineData("./TestFiles/YamlContainsOnlyMaxCreatedDate.md")] + public void GetCreatedDate_YamlContainsMaxDate_ReturnsCreatedDate(string testFilePath) + { + // Arrange + var testOptions = Options.Create(new FrontMatterOptions()); + var expectedDateUtc = DateTime.MaxValue; - // Assert - createdDate.Should().Be(expectedDateUtc); - } + // Act + var markdownFrontMatter = new MarkdownFrontMatter(testOptions); + var createdDate = markdownFrontMatter.GetCreatedDateUtc(testFilePath); - private void SetTimeZone(string mockTimeZoneId) - { - var timeZone = TimeZoneInfo.FindSystemTimeZoneById(mockTimeZoneId); - var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static); - Debug.Assert(info != null, nameof(info) + " != null"); + // Assert + createdDate.Should().Be(expectedDateUtc); + } + + private static void SetTimeZone(string mockTimeZoneId) + { + var timeZone = TimeZoneInfo.FindSystemTimeZoneById(mockTimeZoneId); + var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static); + Debug.Assert(info != null, nameof(info) + " != null"); - var cachedData = info.GetValue(null); - Debug.Assert(cachedData != null, nameof(cachedData) + " != null"); + var cachedData = info.GetValue(null); + Debug.Assert(cachedData != null, nameof(cachedData) + " != null"); - var field = cachedData.GetType().GetField("_localTimeZone", - BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance); - Debug.Assert(field != null, nameof(field) + " != null"); + var field = cachedData.GetType().GetField("_localTimeZone", + BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance); + Debug.Assert(field != null, nameof(field) + " != null"); - field.SetValue(cachedData, timeZone); - } + field.SetValue(cachedData, timeZone); + } - public void Dispose() - { - TimeZoneInfo.ClearCachedData(); - } + public void Dispose() + { + TimeZoneInfo.ClearCachedData(); } -} +} \ No newline at end of file