diff --git a/source/Commands/NewSiteCommand.cs b/source/Commands/NewSiteCommand.cs index e2010712b21043a430b2aabf1cc2e2696c0e1cd5..3b9ae662ea27fe8f99e09afb83ef7eb89e9e6f2e 100644 --- a/source/Commands/NewSiteCommand.cs +++ b/source/Commands/NewSiteCommand.cs @@ -1,45 +1,59 @@ using Serilog; using SuCoS.Models; using SuCoS.Models.CommandLineOptions; +using SuCoS.Parser; namespace SuCoS; /// /// Check links of a given site. /// -public sealed partial class NewSiteCommand(NewSiteOptions options, ILogger logger) +public sealed partial class NewSiteCommand(NewSiteOptions options, ILogger logger, IFileSystem fileSystem, ISite site) { + private static SiteSettings siteSettings = null!; + /// - /// Run the app + /// Generate the needed data for the class /// + /// + /// + /// /// - public int Run() + public static NewSiteCommand Create(NewSiteOptions options, ILogger logger, IFileSystem fileSystem) { - var siteSettings = new SiteSettings() + ArgumentNullException.ThrowIfNull(options); + + siteSettings = new SiteSettings() { Title = options.Title, Description = options.Description, BaseURL = options.BaseURL }; - // TODO: Refactor Site class to not need YAML parser nor FrontMatterParser - var site = new Site(new ServeOptions() { SourceOption = options.Output }, siteSettings, null!, logger, null); + var site = new Site(new GenerateOptions() { SourceOption = options.Output }, siteSettings, new YAMLParser(), null!, null); + return new NewSiteCommand(options, logger, fileSystem, site); + } - var outputPath = Path.GetFullPath(options.Output); - var siteSettingsPath = Path.Combine(outputPath, "sucos.yaml"); + /// + /// Run the app + /// + /// + public int Run() + { + var outputPath = fileSystem.GetFullPath(options.Output); + var siteSettingsPath = fileSystem.Combine(outputPath, "sucos.yaml"); - if (File.Exists(siteSettingsPath) && !options.Force) + if (fileSystem.FileExists(siteSettingsPath) && !options.Force) { logger.Error("{directoryPath} already exists", outputPath); return 1; } - logger.Information("Creating a new site: {title} at {outputPath}", siteSettings.Title, outputPath); - - CreateFolders(site.SourceFodlers); + logger.Information("Creating a new site: {title} at {outputPath}", site.Title, outputPath); try { + CreateFolders(site.SourceFolders); site.Parser.Export(siteSettings, siteSettingsPath); } catch (Exception ex) @@ -61,7 +75,7 @@ public sealed partial class NewSiteCommand(NewSiteOptions options, ILogger logge foreach (var folder in folders) { logger.Information("Creating {folder}", folder); - Directory.CreateDirectory(folder); + fileSystem.CreateDirectory(folder); } } } \ No newline at end of file diff --git a/source/Helpers/IFileSystem.cs b/source/Helpers/IFileSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..b6e5eeb531d5d6b2b09dae02e84cef47572ba6ac --- /dev/null +++ b/source/Helpers/IFileSystem.cs @@ -0,0 +1,53 @@ +namespace SuCoS; + +/// +/// Interface for the System.File class +/// +public interface IFileSystem +{ + /// + /// Path.GetFullPath + /// + /// + /// + string GetFullPath(string path); + + /// + /// Path.Combine + /// + /// + /// + /// + string Combine(string path1, string path2); + + /// + /// File.Exists + /// + /// + /// + bool FileExists(string path); + + /// + /// Directory.CreateDirectory + /// + /// + void CreateDirectory(string path); +} + +/// +/// Actual implementation of FS operations +/// +public class FileSystem : IFileSystem +{ + /// + public string GetFullPath(string path) => Path.GetFullPath(path); + + /// + public string Combine(string path1, string path2) => Path.Combine(path1, path2); + + /// + public bool FileExists(string path) => File.Exists(path); + + /// + public void CreateDirectory(string path) => Directory.CreateDirectory(path); +} diff --git a/source/Models/CommandLineOptions/NewSiteOptions.cs b/source/Models/CommandLineOptions/NewSiteOptions.cs index f0dc375fb9232c26ce8d7c6989d0ec5895288704..6584a1ef0ac61bd22c52c2a75650ab4673d63d5d 100644 --- a/source/Models/CommandLineOptions/NewSiteOptions.cs +++ b/source/Models/CommandLineOptions/NewSiteOptions.cs @@ -24,17 +24,17 @@ public class NewSiteOptions /// Site title. /// [Option("title", Required = false, HelpText = "Site title")] - public required string Title { get; init; } = "My Site"; + public string Title { get; init; } = "My Site"; /// /// Site description. /// [Option("description", Required = false, HelpText = "Site description")] - public required string Description { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; /// /// Site base url. /// [Option("url", Required = false, HelpText = "Site base url")] - public required string BaseURL { get; init; } = "https://example.org/"; + public string BaseURL { get; init; } = "https://example.org/"; } diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs index 40cab16bb5c6aae7c1288ac1fb846812e97b636a..cdbc88be69429de864f4948ce585904660c9cc38 100644 --- a/source/Models/ISite.cs +++ b/source/Models/ISite.cs @@ -72,6 +72,11 @@ public interface ISite : ISiteSettings, IParams /// public ILogger Logger { get; } + /// + /// List of all basic source folders + /// + public IEnumerable SourceFolders { get; } + /// /// Resets the template cache to force a reload of all templates. /// diff --git a/source/Models/Site.cs b/source/Models/Site.cs index a70d290c117b88139ae321df3c5904adb471bb50..c315c2b70a55200431dda55f2ecf099425bcefd0 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -1,4 +1,3 @@ -using CommandLine; using Serilog; using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; @@ -64,10 +63,8 @@ public class Site : ISite /// public string SourceThemePath => Path.Combine(Options.Source, settings.ThemeDir, settings.Theme ?? string.Empty); - /// - /// List of all basic source folders - /// - public IEnumerable SourceFodlers => [ + /// + public IEnumerable SourceFolders => [ SourceContentPath, SourceStaticPath, SourceThemePath @@ -167,13 +164,13 @@ public class Site : ISite public Site( in IGenerateOptions options, in SiteSettings settings, - in IMetadataParser frontMatterParser, + in IMetadataParser parser, in ILogger logger, ISystemClock? clock) { Options = options; this.settings = settings; Logger = logger; - Parser = frontMatterParser; + Parser = parser; TemplateEngine = new FluidTemplateEngine(); this.clock = clock ?? new SystemClock(); diff --git a/source/Program.cs b/source/Program.cs index d99daa7148f2ed68744affc87850641220f27d57..c6cf3f078d7443a21c5bfa7c14ad11bca3230cb6 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -103,7 +103,8 @@ public class Program(ILogger logger) }, (NewSiteOptions options) => { - var command = new NewSiteCommand(options, logger); + var fileSystem = new FileSystem(); + var command = NewSiteCommand.Create(options, logger, fileSystem); return Task.FromResult(command.Run()); }, (NewThemeOptions options) => diff --git a/source/YamlDotNet.Analyzers.StaticGenerator/.editorconfig b/source/YamlDotNet.Analyzers.StaticGenerator/.editorconfig deleted file mode 100644 index 0489b3488417452a4e0247f4c1ed7636c5706c04..0000000000000000000000000000000000000000 --- a/source/YamlDotNet.Analyzers.StaticGenerator/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.cs] -dotnet_diagnostic.CS1591.severity = none # disable on debug genereated files diff --git a/test/BaseGeneratorCommandTests.cs b/test/Commands/BaseGeneratorCommandTests.cs similarity index 86% rename from test/BaseGeneratorCommandTests.cs rename to test/Commands/BaseGeneratorCommandTests.cs index 4aa4d7c513f878f8cf46e02e2f30a5b398029309..28040d2a418ba264bdbb6e0f0a62b3d63465d30d 100644 --- a/test/BaseGeneratorCommandTests.cs +++ b/test/Commands/BaseGeneratorCommandTests.cs @@ -5,7 +5,7 @@ using SuCoS.TemplateEngine; using System.Reflection; using Xunit; -namespace Tests; +namespace Tests.Commands; public class BaseGeneratorCommandTests { @@ -16,11 +16,8 @@ public class BaseGeneratorCommandTests private static readonly ILogger testLogger = new LoggerConfiguration().CreateLogger(); - private class BaseGeneratorCommandStub : BaseGeneratorCommand - { - public BaseGeneratorCommandStub(IGenerateOptions options, ILogger logger) - : base(options, logger) { } - } + private class BaseGeneratorCommandStub(IGenerateOptions options, ILogger logger) + : BaseGeneratorCommand(options, logger); [Fact] public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() diff --git a/test/Commands/NewSiteCommandTests.cs b/test/Commands/NewSiteCommandTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..af1c3712911aea8620fa4b2068347669e61af4a4 --- /dev/null +++ b/test/Commands/NewSiteCommandTests.cs @@ -0,0 +1,194 @@ +using SuCoS; +using SuCoS.Models; +using SuCoS.Models.CommandLineOptions; +using Xunit; +using NSubstitute; +using Serilog; + +namespace Tests.Commands; + +public class NewSiteCommandTests +{ + readonly ILogger logger; + readonly IFileSystem fileSystem; + readonly ISite site; + + public NewSiteCommandTests() + { + logger = Substitute.For(); + fileSystem = Substitute.For(); + site = Substitute.For(); + } + + [Fact] + public void Create_ShouldReturnNewSiteCommand_WhenParametersAreValid() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Title = "Test", Description = "Test", BaseURL = "http://test.com" }; + + // Act + var result = NewSiteCommand.Create(options, logger, fileSystem); + + // Assert + Assert.IsType(result); + } + + [Fact] + public void Create_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Act and Assert + Assert.Throws(testCode: () => NewSiteCommand.Create(null!, logger, fileSystem)); + } + + [Fact] + public void Create_ShouldNotReturnNull_WhenParametersAreValid() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Title = "Test", Description = "Test", BaseURL = "http://test.com" }; + + // Act + var result = NewSiteCommand.Create(options, logger, fileSystem); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void Run_ShouldLogInformation_WhenCreatingNewSite() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Title = "Test", Description = "Test", BaseURL = "http://test.com", Force = false }; + fileSystem.FileExists(Arg.Any()).Returns(false); + + var newSiteCommand = NewSiteCommand.Create(options, logger, fileSystem); + + // Act + newSiteCommand.Run(); + + // Assert + logger.Received(1).Information("Creating a new site: {title} at {outputPath}", options.Title, Arg.Any()); + } + + [Fact] + public void Run_ShouldCallCreateDirectoryWithCorrectPaths_ForEachFolder() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Title = "Test", Description = "Test", BaseURL = "http://test.com", Force = false }; + var site = Substitute.For(); + site.SourceFolders.Returns(["folder1", "folder2"]); + fileSystem.FileExists(Arg.Any()).Returns(false); + + var newSiteCommand = new NewSiteCommand(options, logger, fileSystem, site); + + // Act + newSiteCommand.Run(); + + // Assert + fileSystem.Received(1).CreateDirectory("folder1"); + fileSystem.Received(1).CreateDirectory("folder2"); + } + + [Fact] + public void Run_ShouldReturn1_WhenCreateDirectoryThrowsException() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Title = "Test", Description = "Test", BaseURL = "http://test.com", Force = false }; + var site = Substitute.For(); + site.SourceFolders.Returns(["folder1", "folder2"]); + fileSystem.FileExists(Arg.Any()).Returns(false); + fileSystem.When(x => x.CreateDirectory(Arg.Any())) + .Do(x => { throw new ArgumentNullException(); }); + + var newSiteCommand = new NewSiteCommand(options, logger, fileSystem, site); + + // Act + var result = newSiteCommand.Run(); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Run_ShouldReturn0_WhenDirectoryDoesNotExist() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Force = false }; + fileSystem.FileExists(Arg.Any()).Returns(false); + + var newSiteCommand = new NewSiteCommand(options, logger, fileSystem, site); + + // Act + var result = newSiteCommand.Run(); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Run_ShouldReturn1_WhenForceIsFalseAndDirectoryExists() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Force = false }; + fileSystem.FileExists(Arg.Any()).Returns(true); + + var newSiteCommand = new NewSiteCommand(options, logger, fileSystem, site); + + // Act + var result = newSiteCommand.Run(); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Run_ShouldReturn0_WhenForceIsTrueAndDirectoryExists() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Force = true }; + fileSystem.FileExists(Arg.Any()).Returns(true); + + var newSiteCommand = new NewSiteCommand(options, logger, fileSystem, site); + + // Act + var result = newSiteCommand.Run(); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Run_ShouldReturn1_WhenExportThrowsException() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Force = true }; + fileSystem.FileExists(Arg.Any()).Returns(false); + site.Parser + .When(x => x.Export(Arg.Any(), Arg.Any())) + .Do(x => { throw new ArgumentNullException(); }); + + var newSiteCommand = new NewSiteCommand(options, logger, fileSystem, site); + + // Act + var result = newSiteCommand.Run(); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Run_ShouldCallCreateDirectory_ForEachFolder() + { + // Arrange + var options = new NewSiteOptions { Output = "test", Force = false }; + site.SourceFolders.Returns(["folder1", "folder2"]); + fileSystem.FileExists(Arg.Any()).Returns(false); + + var newSiteCommand = new NewSiteCommand(options, logger, fileSystem, site); + + // Act + newSiteCommand.Run(); + + // Assert + fileSystem.Received(2).CreateDirectory(Arg.Any()); + } +}