From 4c53a276a8b3e4902bd6c1d8c2a949dd76d3369b Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 12 Jul 2023 17:08:25 -0300 Subject: [PATCH 1/4] refactor: server command handlers and tests --- source/Helpers/SiteHelper.cs | 2 +- source/Helpers/SourceFileWatcher.cs | 64 +++++++ .../Models/CommandLineOptions/BuildOptions.cs | 8 +- source/Program.cs | 66 +++---- source/ServeCommand.cs | 161 +++--------------- source/ServerHandlers/IServerHandlers.cs | 28 +++ source/ServerHandlers/PingRequests.cs | 31 ++++ .../ServerHandlers/RegisteredPageRequest.cs | 68 ++++++++ source/ServerHandlers/StaticFileRequest.cs | 66 +++++++ test/.TestSites/01/sucos.yaml | 1 + test/.TestSites/02-have-index/sucos.yaml | 1 + test/.TestSites/03-section/sucos.yaml | 1 + test/.TestSites/04-tags/sucos.yaml | 1 + test/.TestSites/05-theme-no-baseof/sucos.yaml | 1 + test/.TestSites/06-theme/sucos.yaml | 1 + .../07-theme-no-baseof-error/sucos.yaml | 1 + .../08-theme-html/content/blog/alias.md | 8 + .../08-theme-html/content/blog/categories.md | 6 + .../08-theme-html/content/blog/date-future.md | 8 + .../08-theme-html/content/blog/date-ok.md | 8 + .../08-theme-html/content/blog/expired.md | 8 + .../content/blog/publishdate-future.md | 8 + .../content/blog/publishdate-ok.md | 8 + .../08-theme-html/content/blog/tags-01.md | 8 + .../08-theme-html/content/blog/tags-02.md | 8 + .../08-theme-html/content/blog/test01.md | 5 + .../content/blog/weight-negative-1.md | 6 + .../content/blog/weight-negative-100.md | 6 + .../content/blog/weight-positive-1.md | 6 + .../content/blog/weight-positive-100.md | 6 + .../.TestSites/08-theme-html/content/index.md | 5 + test/.TestSites/08-theme-html/index.md | 5 + test/.TestSites/08-theme-html/sucos.yaml | 1 + .../theme/_default/baseof.liquid | 7 + .../08-theme-html/theme/_default/index.liquid | 1 + .../08-theme-html/theme/_default/list.liquid | 1 + .../theme/_default/single.liquid | 1 + test/Models/FrontMatterTests.cs | 2 +- test/Models/PageTests.cs | 24 +-- test/Models/SiteTests.cs | 32 +--- .../ServerHandlers/PingRequestHandlerTests.cs | 48 ++++++ .../RegisteredPageRequestHandlerTests.cs | 72 ++++++++ .../StaticFileRequestHandlerTests.cs | 68 ++++++++ test/TestSetup.cs | 46 +++++ 44 files changed, 691 insertions(+), 222 deletions(-) create mode 100644 source/Helpers/SourceFileWatcher.cs create mode 100644 source/ServerHandlers/IServerHandlers.cs create mode 100644 source/ServerHandlers/PingRequests.cs create mode 100644 source/ServerHandlers/RegisteredPageRequest.cs create mode 100644 source/ServerHandlers/StaticFileRequest.cs create mode 100644 test/.TestSites/01/sucos.yaml create mode 100644 test/.TestSites/02-have-index/sucos.yaml create mode 100644 test/.TestSites/03-section/sucos.yaml create mode 100644 test/.TestSites/04-tags/sucos.yaml create mode 100644 test/.TestSites/05-theme-no-baseof/sucos.yaml create mode 100644 test/.TestSites/06-theme/sucos.yaml create mode 100644 test/.TestSites/07-theme-no-baseof-error/sucos.yaml create mode 100644 test/.TestSites/08-theme-html/content/blog/alias.md create mode 100644 test/.TestSites/08-theme-html/content/blog/categories.md create mode 100644 test/.TestSites/08-theme-html/content/blog/date-future.md create mode 100644 test/.TestSites/08-theme-html/content/blog/date-ok.md create mode 100644 test/.TestSites/08-theme-html/content/blog/expired.md create mode 100644 test/.TestSites/08-theme-html/content/blog/publishdate-future.md create mode 100644 test/.TestSites/08-theme-html/content/blog/publishdate-ok.md create mode 100644 test/.TestSites/08-theme-html/content/blog/tags-01.md create mode 100644 test/.TestSites/08-theme-html/content/blog/tags-02.md create mode 100644 test/.TestSites/08-theme-html/content/blog/test01.md create mode 100644 test/.TestSites/08-theme-html/content/blog/weight-negative-1.md create mode 100644 test/.TestSites/08-theme-html/content/blog/weight-negative-100.md create mode 100644 test/.TestSites/08-theme-html/content/blog/weight-positive-1.md create mode 100644 test/.TestSites/08-theme-html/content/blog/weight-positive-100.md create mode 100644 test/.TestSites/08-theme-html/content/index.md create mode 100644 test/.TestSites/08-theme-html/index.md create mode 100644 test/.TestSites/08-theme-html/sucos.yaml create mode 100644 test/.TestSites/08-theme-html/theme/_default/baseof.liquid create mode 100644 test/.TestSites/08-theme-html/theme/_default/index.liquid create mode 100644 test/.TestSites/08-theme-html/theme/_default/list.liquid create mode 100644 test/.TestSites/08-theme-html/theme/_default/single.liquid create mode 100644 test/ServerHandlers/PingRequestHandlerTests.cs create mode 100644 test/ServerHandlers/RegisteredPageRequestHandlerTests.cs create mode 100644 test/ServerHandlers/StaticFileRequestHandlerTests.cs create mode 100644 test/TestSetup.cs diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 33d31b9..910030e 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -41,7 +41,7 @@ public static class SiteHelper } catch { - throw new FormatException("Error reading app config"); + throw new FormatException($"Error reading app config {configFile}"); } var site = new Site(options, siteSettings, frontMatterParser, logger, null); diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs new file mode 100644 index 0000000..d26b5c2 --- /dev/null +++ b/source/Helpers/SourceFileWatcher.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; + +/// +/// The FileSystemWatcher object that monitors the source directory for file changes. +/// +public interface IFileWatcher +{ + /// + /// Starts the file watcher to monitor file changes in the specified source path. + /// + /// The path to the source directory. + /// + /// The created FileSystemWatcher object. + void Start(string SourceAbsolutePath, Action OnSourceFileChanged); + + /// + /// Disposes the file watcher + /// + void Stop(); +} + +/// +/// The FileSystemWatcher object that monitors the source directory for file changes. +/// +public class SourceFileWatcher : IFileWatcher +{ + /// + /// The FileSystemWatcher object that monitors the source directory for file changes. + /// When a change is detected, this triggers a server restart to ensure the served content + /// remains up-to-date. The FileSystemWatcher is configured with the source directory + /// at construction and starts watching immediately. + /// + private FileSystemWatcher? fileWatcher; + + /// + public void Start(string SourceAbsolutePath, Action OnSourceFileChanged) + { + if (OnSourceFileChanged is null) + { + throw new ArgumentNullException(nameof(OnSourceFileChanged)); + } + + fileWatcher = new FileSystemWatcher + { + Path = SourceAbsolutePath, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + IncludeSubdirectories = true, + EnableRaisingEvents = true + }; + + // Subscribe to the desired events + fileWatcher.Changed += new FileSystemEventHandler(OnSourceFileChanged.Invoke); + fileWatcher.Created += new FileSystemEventHandler(OnSourceFileChanged.Invoke); + fileWatcher.Deleted += new FileSystemEventHandler(OnSourceFileChanged.Invoke); + fileWatcher.Renamed += new RenamedEventHandler(OnSourceFileChanged); + } + + /// + public void Stop() + { + fileWatcher?.Dispose(); + } +} diff --git a/source/Models/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs index 208de26..3710cdf 100644 --- a/source/Models/CommandLineOptions/BuildOptions.cs +++ b/source/Models/CommandLineOptions/BuildOptions.cs @@ -1,3 +1,5 @@ +using System.IO; + namespace SuCoS.Models.CommandLineOptions; /// @@ -13,9 +15,11 @@ public class BuildOptions : GenerateOptions /// /// Constructor /// + /// /// - public BuildOptions(string output) + public BuildOptions(string source, string output) { - Output = output; + Source = source; + Output = string.IsNullOrEmpty(output) ? Path.Combine(source, "public") : output; } } diff --git a/source/Program.cs b/source/Program.cs index dfacd22..4f9658e 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using System.IO; using System.Reflection; using System.Threading.Tasks; using Serilog; @@ -15,14 +14,6 @@ public class Program { private ILogger logger; - /// - /// Constructor - /// - private Program(ILogger logger) - { - this.logger = logger; - } - /// /// Entry point of the program /// @@ -30,25 +21,24 @@ public class Program /// public static int Main(string[] args) { - ILogger logger = new LoggerConfiguration() - .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) - .CreateLogger(); - + var logger = CreateLogger(); var program = new Program(logger); return program.Run(args); } + /// + /// Constructor + /// + private Program(ILogger logger) + { + this.logger = logger; + } + private int Run(string[] args) { // Print the logo of the program. OutputLogo(); - - // Print the name and version of the program. - var assembly = Assembly.GetEntryAssembly(); - var assemblyName = assembly?.GetName(); - var appName = assemblyName?.Name; - var appVersion = assemblyName?.Version; - logger.Information("{name} v{version}", appName, appVersion); + OutputWelcome(); // Shared options between the commands var sourceOption = new Option(new[] { "--source", "-s" }, () => ".", "Source directory path"); @@ -67,16 +57,14 @@ public class Program }; buildCommandHandler.SetHandler((source, output, future, verbose) => { + logger = CreateLogger(verbose); + BuildOptions buildOptions = new( - output: string.IsNullOrEmpty(output) ? Path.Combine(source, "public") : output) + source: source, + output: output) { - Source = source, Future = future }; - logger = new LoggerConfiguration() - .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) - .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) - .CreateLogger(); _ = new BuildCommand(buildOptions, logger); }, sourceOption, buildOutputOption, futureOption, verboseOption); @@ -90,17 +78,15 @@ public class Program }; serveCommandHandler.SetHandler(async (source, future, verbose) => { + logger = CreateLogger(verbose); + ServeOptions serverOptions = new() { Source = source, Future = future }; - logger = new LoggerConfiguration() - .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) - .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) - .CreateLogger(); - var serveCommand = new ServeCommand(serverOptions, logger); + var serveCommand = new ServeCommand(serverOptions, logger, new SourceFileWatcher()); await serveCommand.RunServer(); await Task.Delay(-1); // Wait forever. }, @@ -115,6 +101,24 @@ public class Program return rootCommand.Invoke(args); } + private static ILogger CreateLogger(bool verbose = false) + { + return new LoggerConfiguration() + .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) + .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) + .CreateLogger(); + } + + private void OutputWelcome() + { + // Print the name and version of the program. + var assembly = Assembly.GetEntryAssembly(); + var assemblyName = assembly?.GetName(); + var appName = assemblyName?.Name; + var appVersion = assemblyName?.Version; + logger.Information("{name} v{version}", appName, appVersion); + } + private void OutputLogo() { logger.Information(@" diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index fac2bd6..f82a3fb 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -1,16 +1,14 @@ using System; using System.IO; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.StaticFiles; using Serilog; using SuCoS.Helpers; -using SuCoS.Models; using SuCoS.Models.CommandLineOptions; +using SuCoS.ServerHandlers; namespace SuCoS; @@ -45,13 +43,15 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// private IWebHost? host; + private IServerHandlers[]? handlers; + /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// When a change is detected, this triggers a server restart to ensure the served content /// remains up-to-date. The FileSystemWatcher is configured with the source directory /// at construction and starts watching immediately. /// - private readonly FileSystemWatcher sourceFileWatcher; + private readonly IFileWatcher fileWatcher; /// /// A Timer that helps to manage the frequency of server restarts. @@ -101,6 +101,13 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable serverStartTime = DateTime.UtcNow; + handlers = new IServerHandlers[]{ + new PingRequests(), + new StaticFileRequest(site.SourceStaticPath), + new StaticFileRequest(site.SourceThemeStaticPath), + new RegisteredPageRequest() + }; + host = new WebHostBuilder() .UseKestrel() .UseUrls(baseURLGlobal) @@ -121,7 +128,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable public void Dispose() { host?.Dispose(); - sourceFileWatcher.Dispose(); + fileWatcher.Stop(); debounceTimer?.Dispose(); GC.SuppressFinalize(this); } @@ -131,40 +138,17 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// /// ServeOptions object specifying the serve options. /// The logger instance. Injectable for testing - public ServeCommand(ServeOptions options, ILogger logger) : base(options, logger) + /// + public ServeCommand(ServeOptions options, ILogger logger, IFileWatcher fileWatcher) : base(options, logger) { this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.fileWatcher = fileWatcher ?? throw new ArgumentNullException(nameof(fileWatcher)); baseURLGlobal = $"{baseURLDefault}:{portDefault}"; // Watch for file changes in the specified path - sourceFileWatcher = StartFileWatcher(options.Source); - } - - /// - /// Starts the file watcher to monitor file changes in the specified source path. - /// - /// The path to the source directory. - /// The created FileSystemWatcher object. - private FileSystemWatcher StartFileWatcher(string SourcePath) - { - var SourceAbsolutePath = Path.GetFullPath(SourcePath); - + var SourceAbsolutePath = Path.GetFullPath(options.Source); logger.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); - - var fileWatcher = new FileSystemWatcher - { - Path = SourceAbsolutePath, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, - IncludeSubdirectories = true, - EnableRaisingEvents = true - }; - - // Subscribe to the desired events - fileWatcher.Changed += OnSourceFileChanged; - fileWatcher.Created += OnSourceFileChanged; - fileWatcher.Deleted += OnSourceFileChanged; - fileWatcher.Renamed += OnSourceFileChanged; - return fileWatcher; + fileWatcher.Start(SourceAbsolutePath, OnSourceFileChanged); } /// @@ -205,40 +189,20 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable requestPath = requestPath.TrimEnd('/'); } - var fileAbsolutePath = Path.Combine(site.SourceStaticPath, requestPath.TrimStart('/')); - var fileAbsoluteThemePath = Path.Combine(site.SourceThemeStaticPath, requestPath.TrimStart('/')); - - string? resultType; - - // Return the server startup timestamp as the response - if (requestPath == "/ping") + string? resultType = null; + if (handlers is not null) { - resultType = "ping"; - await HandlePingRequest(context); - } - - // Check if it is one of the Static files (serve the actual file) - else if (File.Exists(fileAbsolutePath)) - { - resultType = "static"; - await HandleStaticFileRequest(context, fileAbsolutePath); - } - - // Check if it is one of the Static files (serve the actual file) - else if (File.Exists(fileAbsoluteThemePath)) - { - resultType = "themestatic"; - await HandleStaticFileRequest(context, fileAbsoluteThemePath); - } - - // Check if the requested file path corresponds to a registered page - else if (site.PagesReferences.TryGetValue(requestPath, out var page)) - { - resultType = "dict"; - await HandleRegisteredPageRequest(context, page); + foreach (var item in handlers) + { + if (item.Check(requestPath, site)) + { + resultType = await item.Handle(context, serverStartTime); + break; + } + } } - else + if (resultType is null) { resultType = "404"; await HandleNotFoundRequest(context); @@ -246,81 +210,12 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable logger.Debug("Request {type}\tfor {RequestPath}", resultType, requestPath); } - private Task HandlePingRequest(HttpContext context) - { - var content = serverStartTime.ToString("o"); - return context.Response.WriteAsync(content); - } - - private static async Task HandleStaticFileRequest(HttpContext context, string fileAbsolutePath) - { - context.Response.ContentType = GetContentType(fileAbsolutePath); - await using var fileStream = new FileStream(fileAbsolutePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.Body); - } - - private async Task HandleRegisteredPageRequest(HttpContext context, Page page) - { - var content = page.CompleteContent; - content = InjectReloadScript(content); - await context.Response.WriteAsync(content); - } - private static async Task HandleNotFoundRequest(HttpContext context) { context.Response.StatusCode = 404; await context.Response.WriteAsync("404 - File Not Found"); } - /// - /// Retrieves the content type of a file based on its extension. - /// If the content type cannot be determined, the default value "application/octet-stream" is returned. - /// - /// The path of the file. - /// The content type of the file. - private static string GetContentType(string filePath) - { - var provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(filePath, out var contentType)) - { - contentType = "application/octet-stream"; - } - return contentType ?? "application/octet-stream"; - } - - /// - /// Injects a reload script into the provided content. - /// The script is read from a JavaScript file and injected before the closing "body" tag. - /// - /// The content to inject the reload script into. - /// The content with the reload script injected. - private string InjectReloadScript(string content) - { - // Read the content of the JavaScript file - string scriptContent; - try - { - var assembly = Assembly.GetExecutingAssembly(); - using var stream = assembly.GetManifestResourceStream("SuCoS.wwwroot.js.reload.js") - ?? throw new FileNotFoundException("Could not find the embedded JavaScript resource."); - using var reader = new StreamReader(stream); - scriptContent = reader.ReadToEnd(); - } - catch (Exception ex) - { - logger.Error(ex, "Could not read the JavaScript file."); - throw; - } - - // Inject the JavaScript content - var reloadScript = $""; - - const string bodyClosingTag = ""; - content = content.Replace(bodyClosingTag, $"{reloadScript}{bodyClosingTag}", StringComparison.InvariantCulture); - - return content; - } - /// /// Handles the file change event from the file watcher. /// diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs new file mode 100644 index 0000000..1ea37bf --- /dev/null +++ b/source/ServerHandlers/IServerHandlers.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using SuCoS.Models; + +namespace SuCoS.ServerHandlers; + +/// +/// Handle server requests +/// +public interface IServerHandlers +{ + /// + /// Check if the condition is met to handle the request + /// + /// + /// + /// + bool Check(string requestPath, Site site); + + /// + /// Process the request + /// + /// + /// + /// + Task Handle(HttpContext context, DateTime serverStartTime); +} diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs new file mode 100644 index 0000000..debf5bc --- /dev/null +++ b/source/ServerHandlers/PingRequests.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using SuCoS.Models; + +namespace SuCoS.ServerHandlers; + +/// +/// Return the server startup timestamp as the response +/// +public class PingRequests : IServerHandlers +{ + /// + public bool Check(string requestPath, Site site) + { + return requestPath == "/ping"; + } + + /// + public async Task Handle(HttpContext context, DateTime serverStartTime) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + var content = serverStartTime.ToString("o"); + await context.Response.WriteAsync(content); + + return "ping"; + } +} diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs new file mode 100644 index 0000000..225425c --- /dev/null +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using SuCoS.Models; + +namespace SuCoS.ServerHandlers; + +/// +/// Return the server startup timestamp as the response +/// +public class RegisteredPageRequest : IServerHandlers +{ + private Page? page; + + /// + public bool Check(string requestPath, Site site) + { + if (site is null) + { + throw new ArgumentNullException(nameof(site)); + } + if (requestPath is null) + { + throw new ArgumentNullException(nameof(requestPath)); + } + return site.PagesReferences.TryGetValue(requestPath, out page); + } + + /// + public async Task Handle(HttpContext context, DateTime serverStartTime) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + var content = page!.CompleteContent; + content = InjectReloadScript(content); + await context.Response.WriteAsync(content); + return "dict"; + } + + /// + /// Injects a reload script into the provided content. + /// The script is read from a JavaScript file and injected before the closing "body" tag. + /// + /// The content to inject the reload script into. + /// The content with the reload script injected. + private string InjectReloadScript(string content) + { + // Read the content of the JavaScript file + string scriptContent; + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream("SuCoS.wwwroot.js.reload.js") + ?? throw new FileNotFoundException("Could not find the embedded JavaScript resource."); + using var reader = new StreamReader(stream); + scriptContent = reader.ReadToEnd(); + + // Inject the JavaScript content + var reloadScript = $""; + + const string bodyClosingTag = ""; + content = content.Replace(bodyClosingTag, $"{reloadScript}{bodyClosingTag}", StringComparison.InvariantCulture); + + return content; + } +} diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs new file mode 100644 index 0000000..7ce9bdf --- /dev/null +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.StaticFiles; +using SuCoS.Models; + +namespace SuCoS.ServerHandlers; + +/// +/// Check if it is one of the Static files (serve the actual file) +/// +public class StaticFileRequest : IServerHandlers +{ + private readonly string basePath; + private string? fileAbsolutePath; + + /// + /// Constructor + /// + /// + public StaticFileRequest(string basePath) + { + this.basePath = basePath; + } + + /// + public bool Check(string requestPath, Site site) + { + if (requestPath is null) + { + throw new ArgumentNullException(nameof(requestPath)); + } + fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); + return File.Exists(fileAbsolutePath); + } + + /// + public async Task Handle(HttpContext context, DateTime serverStartTime) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + context.Response.ContentType = GetContentType(fileAbsolutePath!); + await using var fileStream = new FileStream(fileAbsolutePath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + await fileStream.CopyToAsync(context.Response.Body); + return "static"; + } + + /// + /// Retrieves the content type of a file based on its extension. + /// If the content type cannot be determined, the default value "application/octet-stream" is returned. + /// + /// The path of the file. + /// The content type of the file. + private static string GetContentType(string filePath) + { + var provider = new FileExtensionContentTypeProvider(); + if (!provider.TryGetContentType(filePath, out var contentType)) + { + contentType = "application/octet-stream"; + } + return contentType ?? "application/octet-stream"; + } +} diff --git a/test/.TestSites/01/sucos.yaml b/test/.TestSites/01/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/01/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/02-have-index/sucos.yaml b/test/.TestSites/02-have-index/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/02-have-index/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/03-section/sucos.yaml b/test/.TestSites/03-section/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/03-section/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/04-tags/sucos.yaml b/test/.TestSites/04-tags/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/04-tags/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/05-theme-no-baseof/sucos.yaml b/test/.TestSites/05-theme-no-baseof/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/05-theme-no-baseof/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/06-theme/sucos.yaml b/test/.TestSites/06-theme/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/06-theme/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/07-theme-no-baseof-error/sucos.yaml b/test/.TestSites/07-theme-no-baseof-error/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/07-theme-no-baseof-error/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/content/blog/alias.md b/test/.TestSites/08-theme-html/content/blog/alias.md new file mode 100644 index 0000000..44e58b4 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/alias.md @@ -0,0 +1,8 @@ +--- +Title: Test Alias +Aliases: + - v123 + - "{{ page.Title }}-2" +--- + +Test Alias diff --git a/test/.TestSites/08-theme-html/content/blog/categories.md b/test/.TestSites/08-theme-html/content/blog/categories.md new file mode 100644 index 0000000..4065d67 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/categories.md @@ -0,0 +1,6 @@ +--- +Title: Categories +Categories: ['Test', 'Real Data'] +--- + +Categories diff --git a/test/.TestSites/08-theme-html/content/blog/date-future.md b/test/.TestSites/08-theme-html/content/blog/date-future.md new file mode 100644 index 0000000..5a6a90e --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/date-future.md @@ -0,0 +1,8 @@ +--- +Title: Date Future +Date: 2023-07-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/date-ok.md b/test/.TestSites/08-theme-html/content/blog/date-ok.md new file mode 100644 index 0000000..d4363e7 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/date-ok.md @@ -0,0 +1,8 @@ +--- +Title: Date-OK +Date: 2023-01-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/expired.md b/test/.TestSites/08-theme-html/content/blog/expired.md new file mode 100644 index 0000000..02ae1bb --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/expired.md @@ -0,0 +1,8 @@ +--- +Title: Expired +ExpiryDate: 2020-04-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/publishdate-future.md b/test/.TestSites/08-theme-html/content/blog/publishdate-future.md new file mode 100644 index 0000000..70e8da6 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/publishdate-future.md @@ -0,0 +1,8 @@ +--- +Title: PublishDate Future +PublishDate: 2023-07-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/publishdate-ok.md b/test/.TestSites/08-theme-html/content/blog/publishdate-ok.md new file mode 100644 index 0000000..f4507ff --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/publishdate-ok.md @@ -0,0 +1,8 @@ +--- +Title: PublishDate OK +PublishDate: 2023-01-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/tags-01.md b/test/.TestSites/08-theme-html/content/blog/tags-01.md new file mode 100644 index 0000000..0f79798 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/tags-01.md @@ -0,0 +1,8 @@ +--- +Title: Test Alias +Tags: + - tag1 + - tag 2 +--- + +Test Alias diff --git a/test/.TestSites/08-theme-html/content/blog/tags-02.md b/test/.TestSites/08-theme-html/content/blog/tags-02.md new file mode 100644 index 0000000..afcf002 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/tags-02.md @@ -0,0 +1,8 @@ +--- +Title: Test Alias 2 +Tags: + - tag1 + - tag 2 +--- + +Test Alias diff --git a/test/.TestSites/08-theme-html/content/blog/test01.md b/test/.TestSites/08-theme-html/content/blog/test01.md new file mode 100644 index 0000000..2d3c415 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/test01.md @@ -0,0 +1,5 @@ +--- +Title: Test Content 1 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-negative-1.md b/test/.TestSites/08-theme-html/content/blog/weight-negative-1.md new file mode 100644 index 0000000..974d2c9 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-negative-1.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: -1" +Weight: -1 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-negative-100.md b/test/.TestSites/08-theme-html/content/blog/weight-negative-100.md new file mode 100644 index 0000000..e36b408 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-negative-100.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: -100" +Weight: -100 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-positive-1.md b/test/.TestSites/08-theme-html/content/blog/weight-positive-1.md new file mode 100644 index 0000000..2355b30 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-positive-1.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: plus 1" +Weight: 1 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-positive-100.md b/test/.TestSites/08-theme-html/content/blog/weight-positive-100.md new file mode 100644 index 0000000..08cfa38 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-positive-100.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: plus 100" +Weight: 100 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/index.md b/test/.TestSites/08-theme-html/content/index.md new file mode 100644 index 0000000..28806b1 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/index.md @@ -0,0 +1,5 @@ +--- +Title: My Home Page +--- + +Index Content diff --git a/test/.TestSites/08-theme-html/index.md b/test/.TestSites/08-theme-html/index.md new file mode 100644 index 0000000..28806b1 --- /dev/null +++ b/test/.TestSites/08-theme-html/index.md @@ -0,0 +1,5 @@ +--- +Title: My Home Page +--- + +Index Content diff --git a/test/.TestSites/08-theme-html/sucos.yaml b/test/.TestSites/08-theme-html/sucos.yaml new file mode 100644 index 0000000..e0addbe --- /dev/null +++ b/test/.TestSites/08-theme-html/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/baseof.liquid b/test/.TestSites/08-theme-html/theme/_default/baseof.liquid new file mode 100644 index 0000000..f3513fd --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/baseof.liquid @@ -0,0 +1,7 @@ + + + + +BASEOF-{{ page.Content }} + + \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/index.liquid b/test/.TestSites/08-theme-html/theme/_default/index.liquid new file mode 100644 index 0000000..6b9b3b0 --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/index.liquid @@ -0,0 +1 @@ +INDEX-{{ page.ContentPreRendered }} \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/list.liquid b/test/.TestSites/08-theme-html/theme/_default/list.liquid new file mode 100644 index 0000000..df270cf --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/list.liquid @@ -0,0 +1 @@ +LIST-{{ page.ContentPreRendered }} \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/single.liquid b/test/.TestSites/08-theme-html/theme/_default/single.liquid new file mode 100644 index 0000000..bea53d9 --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/single.liquid @@ -0,0 +1 @@ +SINGLE-{{ page.ContentPreRendered }} \ No newline at end of file diff --git a/test/Models/FrontMatterTests.cs b/test/Models/FrontMatterTests.cs index 8c672f6..b045efb 100644 --- a/test/Models/FrontMatterTests.cs +++ b/test/Models/FrontMatterTests.cs @@ -4,7 +4,7 @@ using Xunit; namespace Test.Models; -public class FrontMatterTests +public class FrontMatterTests : TestSetup { [Theory] [InlineData("Title1", "Section1", "Type1", "URL1", Kind.single)] diff --git a/test/Models/PageTests.cs b/test/Models/PageTests.cs index fbf7431..cc77b7b 100644 --- a/test/Models/PageTests.cs +++ b/test/Models/PageTests.cs @@ -2,27 +2,12 @@ using System.Globalization; using Moq; using SuCoS.Models; using Xunit; -using Serilog; using SuCoS.Models.CommandLineOptions; -using SuCoS.Parser; namespace Test.Models; -public class PageTests +public class PageTests : TestSetup { - private readonly IFrontMatterParser frontMatterParser = new YAMLParser(); - private readonly Mock generateOptionsMock = new(); - private readonly Mock siteSettingsMock = new(); - private readonly Mock loggerMock = new(); - private readonly Mock systemClockMock = new(); - private readonly FrontMatter frontMatterMock = new() - { - Title = titleCONST, - SourcePath = sourcePathCONST - }; - private readonly Site site; - private const string titleCONST = "Test Title"; - private const string sourcePathCONST = "/path/to/file.md"; private const string markdown1CONST = @" # word01 word02 @@ -46,13 +31,6 @@ console.WriteLine('hello word') word03 word04 word05 6 7 eight "; - public PageTests() - { - var testDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); - systemClockMock.Setup(c => c.Now).Returns(testDate); - site = new Site(generateOptionsMock.Object, siteSettingsMock.Object, frontMatterParser, loggerMock.Object, systemClockMock.Object); - } - [Theory] [InlineData("Test Title", "/path/to/file.md", "file", "/path/to")] public void Frontmatter_ShouldCreateWithCorrectProperties(string title, string sourcePath, string sourceFileNameWithoutExtension, string sourcePathDirectory) diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index cefd4df..83403a1 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -1,43 +1,13 @@ using Xunit; -using Moq; -using System.Globalization; -using SuCoS.Models; -using Serilog; using SuCoS.Models.CommandLineOptions; -using SuCoS.Parser; namespace Test.Models; /// /// Unit tests for the Site class. /// -public class SiteTests +public class SiteTests : TestSetup { - private readonly Site site; - private readonly Mock generateOptionsMock = new(); - private readonly Mock siteSettingsMock = new(); - private readonly Mock loggerMock = new(); - private readonly IFrontMatterParser frontMatterParser = new YAMLParser(); - private readonly Mock systemClockMock = new(); - private const string testSitePathCONST01 = ".TestSites/01"; - private const string testSitePathCONST02 = ".TestSites/02-have-index"; - private const string testSitePathCONST03 = ".TestSites/03-section"; - private const string testSitePathCONST04 = ".TestSites/04-tags"; - private const string testSitePathCONST05 = ".TestSites/05-theme-no-baseof"; - private const string testSitePathCONST06 = ".TestSites/06-theme"; - private const string testSitePathCONST07 = ".TestSites/07-theme-no-baseof-error"; - - // based on the compiled test.dll path - // that is typically "bin/Debug/netX.0/test.dll" - private const string testSitesPath = "../../.."; - - public SiteTests() - { - var testDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); - systemClockMock.Setup(c => c.Now).Returns(testDate); - site = new Site(generateOptionsMock.Object, siteSettingsMock.Object, frontMatterParser, loggerMock.Object, systemClockMock.Object); - } - [Theory] [InlineData("test01.md")] [InlineData("date-ok.md")] diff --git a/test/ServerHandlers/PingRequestHandlerTests.cs b/test/ServerHandlers/PingRequestHandlerTests.cs new file mode 100644 index 0000000..ec55d50 --- /dev/null +++ b/test/ServerHandlers/PingRequestHandlerTests.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using SuCoS.ServerHandlers; +using Xunit; + +namespace Tests.ServerHandlers; + +public class PingRequestHandlerTests : TestSetup +{ + [Fact] + public async Task Handle_ReturnsServerStartupTimestamp() + { + // Arrange + var context = new DefaultHttpContext(); + + var stream = new MemoryStream(); + context.Response.Body = stream; + + var pingRequests = new PingRequests(); + + // Act + await pingRequests.Handle(context, todayDate); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + + Assert.Equal(todayDate.ToString("o"), content); + } + + + [Theory] + [InlineData("/ping", true)] + [InlineData("ping", false)] + [InlineData(null, false)] + public void Check_HandlesVariousRequestPaths(string requestPath, bool expectedResult) + { + // Arrange + var pingRequests = new PingRequests(); + + // Act + var result = pingRequests.Check(requestPath, site); + + // Assert + Assert.Equal(expectedResult, result); + } +} + diff --git a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs new file mode 100644 index 0000000..98d0a6a --- /dev/null +++ b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Http; +using SuCoS.Models.CommandLineOptions; +using SuCoS.ServerHandlers; +using Xunit; + +namespace Tests.ServerHandlers; + +public class RegisteredPageRequestHandlerTests : TestSetup +{ + [Theory] + [InlineData("/", true)] + [InlineData("/testPage", false)] + public void Check_ReturnsTrueForRegisteredPage(string requestPath, bool exist) + { + // Arrange + var registeredPageRequest = new RegisteredPageRequest(); + var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSitePathCONST06)); + site.Options = new GenerateOptions + { + Source = siteFullPath + }; + + // Act + site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); + + // Assert + Assert.Equal(exist, registeredPageRequest.Check(requestPath, site)); + } + + [Theory] + [InlineData("/", testSitePathCONST06, false)] + [InlineData("/", testSitePathCONST08, true)] + public async Task Handle_ReturnsExpectedContent2(string requestPath, string testSitePath, bool contains) + { + // Arrange + var registeredPageRequest = new RegisteredPageRequest(); + var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSitePath)); + site.Options = new GenerateOptions + { + Source = siteFullPath + }; + + var context = new DefaultHttpContext(); + var stream = new MemoryStream(); + context.Response.Body = stream; + + // Act + site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); + registeredPageRequest.Check(requestPath, site); + await registeredPageRequest.Handle(context, DateTime.Now); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + + // You may want to adjust this assertion depending on the actual format of your injected script + if (contains) + { + Assert.Contains("", content, StringComparison.InvariantCulture); + } + else + { + Assert.DoesNotContain("", content, StringComparison.InvariantCulture); + Assert.DoesNotContain("", content, StringComparison.InvariantCulture); + } + Assert.Contains("Index Content", content, StringComparison.InvariantCulture); + + } + +} diff --git a/test/ServerHandlers/StaticFileRequestHandlerTests.cs b/test/ServerHandlers/StaticFileRequestHandlerTests.cs new file mode 100644 index 0000000..9824db2 --- /dev/null +++ b/test/ServerHandlers/StaticFileRequestHandlerTests.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Http; +using SuCoS.ServerHandlers; +using Xunit; + +namespace Tests.ServerHandlers; + +public class StaticFileRequestHandlerTests : TestSetup, IDisposable +{ + private readonly string tempFilePath; + + public StaticFileRequestHandlerTests() : base() + { + // Creating a temporary file for testing purposes + tempFilePath = Path.GetTempFileName(); + File.WriteAllText(tempFilePath, "test"); + } + + [Fact] + public void Check_ReturnsTrueForExistingFile() + { + // Arrange + var requestPath = Path.GetFileName(tempFilePath); + var basePath = Path.GetDirectoryName(tempFilePath) + ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); + + var staticFileRequest = new StaticFileRequest(basePath); + + // Act + var result = staticFileRequest.Check(requestPath, site); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task Handle_ReturnsExpectedContent() + { + // Arrange + var requestPath = Path.GetFileName(tempFilePath); + var basePath = Path.GetDirectoryName(tempFilePath) + ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); + var staticFileRequest = new StaticFileRequest(basePath); + + var context = new DefaultHttpContext(); + var stream = new MemoryStream(); + context.Response.Body = stream; + + // Act + staticFileRequest.Check(requestPath, site); + await staticFileRequest.Handle(context, DateTime.Now); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + + Assert.Equal("test", content); + } + + public void Dispose() + { + // Cleaning up the temporary file after tests run + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } +} diff --git a/test/TestSetup.cs b/test/TestSetup.cs new file mode 100644 index 0000000..acf2fd4 --- /dev/null +++ b/test/TestSetup.cs @@ -0,0 +1,46 @@ +using Serilog; +using Moq; +using SuCoS.Models; +using SuCoS.Models.CommandLineOptions; +using SuCoS.Parser; +using System.Globalization; + +public class TestSetup +{ + protected const string titleCONST = "Test Title"; + protected const string sourcePathCONST = "/path/to/file.md"; + protected readonly DateTime todayDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); + protected readonly DateTime futureDate = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); + + protected const string testSitePathCONST01 = ".TestSites/01"; + protected const string testSitePathCONST02 = ".TestSites/02-have-index"; + protected const string testSitePathCONST03 = ".TestSites/03-section"; + protected const string testSitePathCONST04 = ".TestSites/04-tags"; + protected const string testSitePathCONST05 = ".TestSites/05-theme-no-baseof"; + protected const string testSitePathCONST06 = ".TestSites/06-theme"; + protected const string testSitePathCONST07 = ".TestSites/07-theme-no-baseof-error"; + protected const string testSitePathCONST08 = ".TestSites/08-theme-html"; + + protected readonly IFrontMatterParser frontMatterParser = new YAMLParser(); + protected readonly Mock generateOptionsMock = new(); + protected readonly Mock siteSettingsMock = new(); + protected readonly Mock loggerMock = new(); + protected readonly Mock systemClockMock = new(); + protected readonly FrontMatter frontMatterMock = new() + { + Title = titleCONST, + SourcePath = sourcePathCONST + }; + + protected readonly Site site; + + // based on the compiled test.dll path + // that is typically "bin/Debug/netX.0/test.dll" + protected const string testSitesPath = "../../.."; + + public TestSetup() + { + systemClockMock.Setup(c => c.Now).Returns(todayDate); + site = new Site(generateOptionsMock.Object, siteSettingsMock.Object, frontMatterParser, loggerMock.Object, systemClockMock.Object); + } +} \ No newline at end of file -- GitLab From dff00e6592efdeaf51e8c869e0efc5a2b2a3c0be Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 12 Jul 2023 17:21:29 -0300 Subject: [PATCH 2/4] test: stopwatchReporter --- test/Helpers/StopwatchReporterTests.cs | 78 ++++++++++++++++++++++++++ test/test.csproj | 1 + 2 files changed, 79 insertions(+) create mode 100644 test/Helpers/StopwatchReporterTests.cs diff --git a/test/Helpers/StopwatchReporterTests.cs b/test/Helpers/StopwatchReporterTests.cs new file mode 100644 index 0000000..c1b9d7b --- /dev/null +++ b/test/Helpers/StopwatchReporterTests.cs @@ -0,0 +1,78 @@ + +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using Serilog; +using Serilog.Sinks.InMemory; +using SuCoS.Helpers; +using Xunit; + +namespace Test.Helpers; +public class StopwatchReporterTests +{ + private readonly ILogger logger; + private readonly StopwatchReporter stopwatchReporter; + private readonly InMemorySink inMemorySink; + + public StopwatchReporterTests() + { + inMemorySink = new InMemorySink(); + logger = new LoggerConfiguration().WriteTo.Sink(inMemorySink).CreateLogger(); + stopwatchReporter = new StopwatchReporter(logger); + } + + [Fact] + public void Start_InitializesAndStartsStopwatchForStep() + { + // Arrange + var stepName = "TestStep"; + var stopwatchReporter = new StopwatchReporter(new LoggerConfiguration().CreateLogger()); + + // Act + stopwatchReporter.Start(stepName); + + // Assert + var stopwatchField = stopwatchReporter.GetType().GetField("stopwatches", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(stopwatchField); + + var stopwatchDictionary = stopwatchField.GetValue(stopwatchReporter) as Dictionary; + Assert.NotNull(stopwatchDictionary); + + Assert.True(stopwatchDictionary.ContainsKey(stepName)); + Assert.True(stopwatchDictionary[stepName].IsRunning); + } + + [Fact] + public void LogReport_CorrectlyLogsElapsedTime() + { + var stepName = "TestStep"; + var siteTitle = "TestSite"; + + stopwatchReporter.Start(stepName); + Thread.Sleep(123); // Let's wait a bit to simulate some processing. + stopwatchReporter.Stop(stepName, 1); + + stopwatchReporter.LogReport(siteTitle); + + // Assert + var logEvents = inMemorySink.LogEvents; + Assert.NotEmpty(logEvents); + var logMessage = logEvents.First().RenderMessage(CultureInfo.InvariantCulture); + Assert.Contains($"Site '{siteTitle}' created!", logMessage, StringComparison.InvariantCulture); + Assert.Contains(stepName, logMessage, StringComparison.InvariantCulture); + Assert.Contains("123 ms", logMessage, StringComparison.InvariantCulture); // Ensure that our processing time was logged. + } + + [Fact] + public void Stop_ThrowsExceptionWhenStopCalledWithoutStart() + { + var stepName = "TestStep"; + + // Don't call Start for stepName + + // Assert that Stop throws an exception + var exception = Assert.Throws(() => stopwatchReporter.Stop(stepName, 1)); + Assert.Equal($"Step '{stepName}' has not been started.", exception.Message); + } + +} diff --git a/test/test.csproj b/test/test.csproj index 520ac34..24f5f95 100644 --- a/test/test.csproj +++ b/test/test.csproj @@ -12,6 +12,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive -- GitLab From ff6d26bf5fbd55a3fa84218a5f3efc7f67769976 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 12 Jul 2023 21:06:56 -0300 Subject: [PATCH 3/4] test: Program hello world outputs --- source/AssemblyInfo.cs | 3 +++ source/Program.cs | 36 ++++++++++++++------------ test/Helpers/StopwatchReporterTests.cs | 1 + test/ProgramTest.cs | 35 +++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 source/AssemblyInfo.cs create mode 100644 test/ProgramTest.cs diff --git a/source/AssemblyInfo.cs b/source/AssemblyInfo.cs new file mode 100644 index 0000000..52f99c7 --- /dev/null +++ b/source/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("test")] diff --git a/source/Program.cs b/source/Program.cs index 4f9658e..cabc01a 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -10,8 +10,18 @@ namespace SuCoS; /// /// The main entry point of the program. /// -public class Program +internal class Program { + internal const string helloWorld = @" + ____ ____ ____ +/\ _`\ /\ _`\ /\ _`\ +\ \,\L\_\ __ __\ \ \/\_\ ___\ \,\L\_\ + \/_\__ \ /\ \/\ \\ \ \/_/_ / __`\/_\__ \ + /\ \L\ \ \ \_\ \\ \ \L\ \/\ \L\ \/\ \L\ \ + \ `\____\ \____/ \ \____/\ \____/\ `\____\ + \/_____/\/___/ \/___/ \/___/ \/_____/ +"; + private ILogger logger; /// @@ -29,12 +39,12 @@ public class Program /// /// Constructor /// - private Program(ILogger logger) + public Program(ILogger logger) { this.logger = logger; } - private int Run(string[] args) + internal int Run(string[] args) { // Print the logo of the program. OutputLogo(); @@ -101,7 +111,7 @@ public class Program return rootCommand.Invoke(args); } - private static ILogger CreateLogger(bool verbose = false) + internal static ILogger CreateLogger(bool verbose = false) { return new LoggerConfiguration() .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) @@ -109,9 +119,11 @@ public class Program .CreateLogger(); } - private void OutputWelcome() + /// + /// Print the name and version of the program. + /// + internal void OutputWelcome() { - // Print the name and version of the program. var assembly = Assembly.GetEntryAssembly(); var assemblyName = assembly?.GetName(); var appName = assemblyName?.Name; @@ -119,16 +131,8 @@ public class Program logger.Information("{name} v{version}", appName, appVersion); } - private void OutputLogo() + internal void OutputLogo() { - logger.Information(@" - ____ ____ ____ -/\ _`\ /\ _`\ /\ _`\ -\ \,\L\_\ __ __\ \ \/\_\ ___\ \,\L\_\ - \/_\__ \ /\ \/\ \\ \ \/_/_ / __`\/_\__ \ - /\ \L\ \ \ \_\ \\ \ \L\ \/\ \L\ \/\ \L\ \ - \ `\____\ \____/ \ \____/\ \____/\ `\____\ - \/_____/\/___/ \/___/ \/___/ \/_____/ -"); + logger.Information(helloWorld); } } diff --git a/test/Helpers/StopwatchReporterTests.cs b/test/Helpers/StopwatchReporterTests.cs index c1b9d7b..f68743e 100644 --- a/test/Helpers/StopwatchReporterTests.cs +++ b/test/Helpers/StopwatchReporterTests.cs @@ -8,6 +8,7 @@ using SuCoS.Helpers; using Xunit; namespace Test.Helpers; + public class StopwatchReporterTests { private readonly ILogger logger; diff --git a/test/ProgramTest.cs b/test/ProgramTest.cs new file mode 100644 index 0000000..13d1a2a --- /dev/null +++ b/test/ProgramTest.cs @@ -0,0 +1,35 @@ +using Moq; +using Serilog.Events; +using SuCoS; +using Xunit; + +namespace Test; + +public class ProgramTests : TestSetup +{ + [Theory] + [InlineData(false, LogEventLevel.Information)] + [InlineData(true, LogEventLevel.Debug)] + public void CreateLogger_SetsLogLevel(bool verbose, LogEventLevel expected) + { + // Act + var logger = Program.CreateLogger(verbose); + + // Assert + Assert.True(logger.IsEnabled(expected)); + } + + [Fact] + public void OutputLogo_Should_LogHelloWorld() + { + // Arrange + var program = new Program(loggerMock.Object); + + // Act + program.OutputLogo(); + program.OutputWelcome(); + + // Assert + loggerMock.Verify(x => x.Information(Program.helloWorld), Times.Once); + } +} \ No newline at end of file -- GitLab From 6591d4d92408057049bd1c3c12d5950763a076ee Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 12 Jul 2023 22:51:34 -0300 Subject: [PATCH 4/4] refactor: convert almost all to internal --- source/BaseGeneratorCommand.cs | 2 +- source/BuildCommand.cs | 2 +- source/Helpers/FileUtils.cs | 2 +- source/Helpers/SiteHelper.cs | 2 +- source/Helpers/SourceFileWatcher.cs | 4 +- source/Helpers/StopwatchReporter.cs | 2 +- source/Helpers/Urlizer.cs | 4 +- .../Models/CommandLineOptions/BuildOptions.cs | 2 +- .../CommandLineOptions/GenerateOptions.cs | 2 +- .../Models/CommandLineOptions/ServeOptions.cs | 2 +- source/Models/FrontMatter.cs | 2 +- source/Models/IPage.cs | 126 +++++++++++++++++ source/Models/ISite.cs | 133 ++++++++++++++++++ source/Models/ISystemClock.cs | 2 +- source/Models/Page.cs | 18 +-- source/Models/Site.cs | 40 +++--- source/Models/SiteCacheManager.cs | 2 +- source/Parser/IFrontmatterParser.cs | 4 +- source/Parser/YAMLParser.cs | 8 +- source/ServeCommand.cs | 12 +- source/ServerHandlers/IServerHandlers.cs | 9 +- source/ServerHandlers/PingRequests.cs | 9 +- .../ServerHandlers/RegisteredPageRequest.cs | 20 +-- source/ServerHandlers/StaticFileRequest.cs | 18 +-- .../ServerHandlers/PingRequestHandlerTests.cs | 4 +- .../RegisteredPageRequestHandlerTests.cs | 11 +- .../StaticFileRequestHandlerTests.cs | 14 +- test/TestSetup.cs | 4 +- 28 files changed, 361 insertions(+), 99 deletions(-) create mode 100644 source/Models/IPage.cs create mode 100644 source/Models/ISite.cs diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index dcafd2e..fb05427 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -14,7 +14,7 @@ namespace SuCoS; /// /// Base class for build and serve commands. /// -public abstract class BaseGeneratorCommand +internal abstract class BaseGeneratorCommand { /// /// The configuration file name. diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs index 64db956..9046589 100644 --- a/source/BuildCommand.cs +++ b/source/BuildCommand.cs @@ -10,7 +10,7 @@ namespace SuCoS; /// /// Build Command will build the site based on the source files. /// -public class BuildCommand : BaseGeneratorCommand +internal class BuildCommand : BaseGeneratorCommand { private readonly BuildOptions options; diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 25c490e..217a4bc 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -9,7 +9,7 @@ namespace SuCoS.Helpers; /// /// Helper methods for scanning files. /// -public static class FileUtils +internal static class FileUtils { /// /// Gets the content of a template file based on the page and the theme path. diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 910030e..3ec93a3 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -13,7 +13,7 @@ namespace SuCoS.Helpers; /// /// Helper methods for scanning files. /// -public static class SiteHelper +internal static class SiteHelper { /// /// Markdig 20+ built-in extensions diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs index d26b5c2..d32782b 100644 --- a/source/Helpers/SourceFileWatcher.cs +++ b/source/Helpers/SourceFileWatcher.cs @@ -4,7 +4,7 @@ using System.IO; /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// -public interface IFileWatcher +internal interface IFileWatcher { /// /// Starts the file watcher to monitor file changes in the specified source path. @@ -23,7 +23,7 @@ public interface IFileWatcher /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// -public class SourceFileWatcher : IFileWatcher +internal class SourceFileWatcher : IFileWatcher { /// /// The FileSystemWatcher object that monitors the source directory for file changes. diff --git a/source/Helpers/StopwatchReporter.cs b/source/Helpers/StopwatchReporter.cs index 46219a6..ec4c5a1 100644 --- a/source/Helpers/StopwatchReporter.cs +++ b/source/Helpers/StopwatchReporter.cs @@ -12,7 +12,7 @@ namespace SuCoS.Helpers; /// The stopwatch is started /// and stopped around parts of the code that we want to measure. /// -public class StopwatchReporter +internal class StopwatchReporter { private readonly ILogger logger; private readonly Dictionary stopwatches; diff --git a/source/Helpers/Urlizer.cs b/source/Helpers/Urlizer.cs index 0b83bfd..2af9da0 100644 --- a/source/Helpers/Urlizer.cs +++ b/source/Helpers/Urlizer.cs @@ -8,7 +8,7 @@ namespace SuCoS.Helpers; /// /// Helper class to convert a string to a URL-friendly string. /// -public static partial class Urlizer +internal static partial class Urlizer { [GeneratedRegex(@"[^a-zA-Z0-9]+")] private static partial Regex UrlizeRegexAlpha(); @@ -68,7 +68,7 @@ public static partial class Urlizer /// Options for the class. /// Basically to force lowercase and to change the replacement character. /// -public class UrlizerOptions +internal class UrlizerOptions { /// /// Force to generate lowercase URLs. diff --git a/source/Models/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs index 3710cdf..86f9748 100644 --- a/source/Models/CommandLineOptions/BuildOptions.cs +++ b/source/Models/CommandLineOptions/BuildOptions.cs @@ -5,7 +5,7 @@ namespace SuCoS.Models.CommandLineOptions; /// /// Command line options for the build command. /// -public class BuildOptions : GenerateOptions +internal class BuildOptions : GenerateOptions { /// /// The path of the output files. diff --git a/source/Models/CommandLineOptions/GenerateOptions.cs b/source/Models/CommandLineOptions/GenerateOptions.cs index 709691b..d14eba2 100644 --- a/source/Models/CommandLineOptions/GenerateOptions.cs +++ b/source/Models/CommandLineOptions/GenerateOptions.cs @@ -3,7 +3,7 @@ namespace SuCoS.Models.CommandLineOptions; /// /// Basic Command line options for the serve and build command. /// -public class GenerateOptions : IGenerateOptions +internal class GenerateOptions : IGenerateOptions { /// public string Source { get; init; } = "."; diff --git a/source/Models/CommandLineOptions/ServeOptions.cs b/source/Models/CommandLineOptions/ServeOptions.cs index 9e2b5c9..8ea518b 100644 --- a/source/Models/CommandLineOptions/ServeOptions.cs +++ b/source/Models/CommandLineOptions/ServeOptions.cs @@ -3,6 +3,6 @@ namespace SuCoS.Models.CommandLineOptions; /// /// Command line options for the serve command. /// -public class ServeOptions : GenerateOptions +internal class ServeOptions : GenerateOptions { } diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index 854c046..8ea9df4 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -9,7 +9,7 @@ namespace SuCoS.Models; /// A scafold structure to help creating system-generated content, like /// tag, section or index pages /// -public class FrontMatter : IFrontMatter +internal class FrontMatter : IFrontMatter { #region IFrontMatter diff --git a/source/Models/IPage.cs b/source/Models/IPage.cs new file mode 100644 index 0000000..98ad810 --- /dev/null +++ b/source/Models/IPage.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using Markdig; +using SuCoS.Helpers; + +namespace SuCoS.Models; + +/// +/// Each page data created from source files or from the system. +/// +public interface IPage : IFrontMatter +{ + /// + new Kind Kind { get; set; } + + /// + /// The source directory of the file. + /// + public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourcePathDirectory) + ? null + : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + + /// + /// Point to the site configuration. + /// + public ISite Site { get; } + + /// + /// Secondary URL patterns to be used to create the url. + /// + public List? AliasesProcessed { get; set; } + + /// + /// The URL for the content. + /// + public string? Permalink { get; set; } + + /// + /// Other content that mention this content. + /// Used to create the tags list and Related Posts section. + /// + public ConcurrentBag PagesReferences { get; } + + /// + /// Other content that mention this content. + /// Used to create the tags list and Related Posts section. + /// + public IPage? Parent { get; set; } + + /// + /// Plain markdown content, without HTML. + /// + public string Plain => Markdown.ToPlainText(RawContent, SiteHelper.MarkdownPipeline); + + /// + /// A list of tags, if any. + /// + public ConcurrentBag TagsReference { get; } + + /// + /// Just a simple check if the current page is the home page + /// + public bool IsHome => Site.Home == this; + + /// + /// Just a simple check if the current page is a section page + /// + public bool IsSection => Type == "section"; + + /// + /// Just a simple check if the current page is a "page" + /// + public bool IsPage => Kind == Kind.single; + + /// + /// The number of words in the main content + /// + public int WordCount => Plain.Split(nonWords, StringSplitOptions.RemoveEmptyEntries).Length; + + private static readonly char[] nonWords = { ' ', ',', ';', '.', '!', '"', '(', ')', '?', '\n', '\r' }; + + /// + /// The markdown content converted to HTML + /// + public string ContentPreRendered { get; } + + /// + /// The processed content. + /// + public string Content { get; } + + /// + /// Creates the output file by applying the theme templates to the page content. + /// + /// The processed output file content. + public string CompleteContent { get; } + + + /// + /// Other content that mention this content. + /// Used to create the tags list and Related Posts section. + /// + public IEnumerable Pages { get; } + + /// + /// List of pages from the content folder. + /// + public IEnumerable RegularPages { get; } + + /// + /// Get all URLs related to this content. + /// + public List Urls + { + get; + } + + /// + /// Gets the Permalink path for the file. + /// + /// The URL to consider. If null use the predefined URL + /// The output path. + public string CreatePermalink(string? URLforce = null); +} diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs new file mode 100644 index 0000000..a78e9e6 --- /dev/null +++ b/source/Models/ISite.cs @@ -0,0 +1,133 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using Fluid; +using Serilog; +using SuCoS.Models.CommandLineOptions; + +namespace SuCoS.Models; + +/// +/// The main configuration of the program, primarily extracted from the app.yaml file. +/// +public interface ISite : IParams +{ + /// + /// Command line options + /// + public IGenerateOptions Options { get; set; } + + #region SiteSettings + + /// + /// Site Title. + /// + public string Title { get; } + + /// + /// The appearance of a URL is either ugly or pretty. + /// + public bool UglyURLs { get; } + + #endregion SiteSettings + + /// + /// The path of the content, based on the source path. + /// + public string SourceContentPath { get; } + + /// + /// The path of the static content (that will be copied as is), based on the source path. + /// + public string SourceStaticPath { get; } + + /// + /// The path theme. + /// + public string SourceThemePath { get; } + + /// + /// The path of the static content (that will be copied as is), based on the theme path. + /// + public string SourceThemeStaticPath => Path.Combine(SourceThemePath, "static"); + + /// + /// List of all pages, including generated. + /// + public IEnumerable Pages { get; } + + /// + /// List of all pages, including generated, by their permalink. + /// + public ConcurrentDictionary PagesReferences { get; } + + /// + /// List of pages from the content folder. + /// + public List RegularPages { get; } + + /// + /// The page of the home page; + /// + public IPage? Home { get; } + + /// + /// Manage all caching lists for the site + /// + public SiteCacheManager CacheManager { get; } + + /// + /// The Fluid parser instance. + /// + public FluidParser FluidParser { get; } + + /// + /// The Fluid/Liquid template options. + /// + public TemplateOptions TemplateOptions { get; } + /// + /// The logger instance. + /// + public ILogger Logger { get; } + + /// + /// Resets the template cache to force a reload of all templates. + /// + public void ResetCache(); + + /// + /// Search recursively for all markdown files in the content folder, then + /// parse their content for front matter meta data and markdown. + /// + /// Folder to scan + /// Folder recursive level + /// Page of the upper directory + /// + public void ParseAndScanSourceFiles(string? directory, int level = 0, IPage? parent = null); + + /// + /// Extra calculation and automatic data for each page. + /// + /// The given page to be processed + /// The parent page, if any + /// + public void PostProcessPage(in IPage page, IPage? parent = null, bool overwrite = false); + + /// + /// Check if the page have a publishing date from the past. + /// + /// Page or front matter + /// options + /// + public bool IsValidDate(in IFrontMatter frontMatter, IGenerateOptions? options); + + /// + /// Check if the page is expired + /// + public bool IsDateExpired(in IFrontMatter frontMatter); + + /// + /// Check if the page is publishable + /// + public bool IsDatePublishable(in IFrontMatter frontMatter); +} \ No newline at end of file diff --git a/source/Models/ISystemClock.cs b/source/Models/ISystemClock.cs index bd38900..f3f9274 100644 --- a/source/Models/ISystemClock.cs +++ b/source/Models/ISystemClock.cs @@ -21,7 +21,7 @@ public interface ISystemClock /// /// Represents a concrete implementation of the ISystemClock interface using the system clock. /// -public class SystemClock : ISystemClock +internal class SystemClock : ISystemClock { /// /// Gets the current local date and time. diff --git a/source/Models/Page.cs b/source/Models/Page.cs index fc793a4..70f360e 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -12,7 +12,7 @@ namespace SuCoS.Models; /// /// Each page data created from source files or from the system. /// -public class Page : IFrontMatter +internal class Page : IPage { private readonly IFrontMatter frontMatter; @@ -90,7 +90,7 @@ public class Page : IFrontMatter /// /// Point to the site configuration. /// - public Site Site { get; } + public ISite Site { get; } /// /// Secondary URL patterns to be used to create the url. @@ -112,7 +112,7 @@ public class Page : IFrontMatter /// Other content that mention this content. /// Used to create the tags list and Related Posts section. /// - public Page? Parent { get; set; } + public IPage? Parent { get; set; } /// /// Plain markdown content, without HTML. @@ -122,7 +122,7 @@ public class Page : IFrontMatter /// /// A list of tags, if any. /// - public ConcurrentBag TagsReference { get; } = new(); + public ConcurrentBag TagsReference { get; } = new(); /// /// Just a simple check if the current page is the home page @@ -174,7 +174,7 @@ public class Page : IFrontMatter /// Other content that mention this content. /// Used to create the tags list and Related Posts section. /// - public IEnumerable Pages + public IEnumerable Pages { get { @@ -195,7 +195,7 @@ public class Page : IFrontMatter /// /// List of pages from the content folder. /// - public IEnumerable RegularPages + public IEnumerable RegularPages { get { @@ -262,14 +262,14 @@ echo page.SourceFileNameWithoutExtension endif -%}"; - private List? regularPagesCache; + private List? regularPagesCache; - private List? pagesCached { get; set; } + private List? pagesCached { get; set; } /// /// Constructor /// - public Page(in IFrontMatter frontMatter, in Site site) + public Page(in IFrontMatter frontMatter, in ISite site) { this.frontMatter = frontMatter; Site = site; diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 03fa08f..65a517d 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -15,7 +15,7 @@ namespace SuCoS.Models; /// /// The main configuration of the program, primarily extracted from the app.yaml file. /// -public class Site : IParams +internal class Site : ISite { #region IParams @@ -32,7 +32,7 @@ public class Site : IParams /// /// Command line options /// - public IGenerateOptions Options; + public IGenerateOptions Options { get; set; } #region SiteSettings @@ -71,7 +71,7 @@ public class Site : IParams /// /// List of all pages, including generated. /// - public IEnumerable Pages + public IEnumerable Pages { get { @@ -85,12 +85,12 @@ public class Site : IParams /// /// List of all pages, including generated, by their permalink. /// - public ConcurrentDictionary PagesReferences { get; } = new(); + public ConcurrentDictionary PagesReferences { get; } = new(); /// /// List of pages from the content folder. /// - public List RegularPages + public List RegularPages { get { @@ -106,22 +106,22 @@ public class Site : IParams /// /// The page of the home page; /// - public Page? Home { get; private set; } + public IPage? Home { get; private set; } /// /// Manage all caching lists for the site /// - public readonly SiteCacheManager CacheManager = new(); + public SiteCacheManager CacheManager { get; } = new(); /// /// The Fluid parser instance. /// - public readonly FluidParser FluidParser = new(); + public FluidParser FluidParser { get; } = new(); /// /// The Fluid/Liquid template options. /// - public readonly TemplateOptions TemplateOptions = new(); + public TemplateOptions TemplateOptions { get; } = new(); /// /// The logger instance. @@ -147,9 +147,9 @@ public class Site : IParams /// private readonly IFrontMatterParser frontMatterParser; - private List? pagesCache; + private List? pagesCache; - private List? regularPagesCache; + private List? regularPagesCache; private readonly SiteSettings settings; @@ -163,8 +163,8 @@ public class Site : IParams /// public Site( in IGenerateOptions options, - in SiteSettings settings, - in IFrontMatterParser frontMatterParser, + in SiteSettings settings, + in IFrontMatterParser frontMatterParser, in ILogger logger, ISystemClock? clock) { Options = options; @@ -197,7 +197,7 @@ public class Site : IParams /// Folder recursive level /// Page of the upper directory /// - public void ParseAndScanSourceFiles(string? directory, int level = 0, Page? parent = null) + public void ParseAndScanSourceFiles(string? directory, int level = 0, IPage? parent = null) { directory ??= SourceContentPath; @@ -217,7 +217,7 @@ public class Site : IParams }); } - private void ParseIndexPage(in string? directory, int level, ref Page? parent, ref string[] markdownFiles) + private void ParseIndexPage(in string? directory, int level, ref IPage? parent, ref string[] markdownFiles) { // Check if the index.md file exists in the current directory var indexPage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file).ToUpperInvariant() == indexFileUpperConst); @@ -253,7 +253,7 @@ public class Site : IParams } } - private Page? ParseSourceFile(in Page? parent, in string filePath) + private IPage? ParseSourceFile(in IPage? parent, in string filePath) { Page? page = null; try @@ -286,7 +286,7 @@ public class Site : IParams /// /// /// The created page for the index. - private Page CreateSystemPage(string relativePath, string title, string? sectionName = null, Page? originalPage = null) + private IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null) { sectionName ??= "section"; var isIndex = string.IsNullOrEmpty(relativePath); @@ -303,9 +303,9 @@ public class Site : IParams var id = frontMatter.URL; // Get or create the page - var lazyPage = CacheManager.automaticContentCache.GetOrAdd(id, new Lazy(() => + var lazyPage = CacheManager.automaticContentCache.GetOrAdd(id, new Lazy(() => { - Page? parent = null; + IPage? parent = null; // Check if we need to create a section, even var sections = (frontMatter.SourcePathDirectory ?? string.Empty).Split('/', StringSplitOptions.RemoveEmptyEntries); if (sections.Length > 1) @@ -346,7 +346,7 @@ public class Site : IParams /// The given page to be processed /// The parent page, if any /// - public void PostProcessPage(in Page page, Page? parent = null, bool overwrite = false) + public void PostProcessPage(in IPage page, IPage? parent = null, bool overwrite = false) { if (page is null) { diff --git a/source/Models/SiteCacheManager.cs b/source/Models/SiteCacheManager.cs index d984fe4..6694fbd 100644 --- a/source/Models/SiteCacheManager.cs +++ b/source/Models/SiteCacheManager.cs @@ -22,7 +22,7 @@ public class SiteCacheManager /// /// Cache for tag page. /// - public readonly ConcurrentDictionary> automaticContentCache = new(); + public readonly ConcurrentDictionary> automaticContentCache = new(); /// /// Resets the template cache to force a reload of all templates. diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs index 1318dfb..0f1e023 100644 --- a/source/Parser/IFrontmatterParser.cs +++ b/source/Parser/IFrontmatterParser.cs @@ -13,7 +13,7 @@ public interface IFrontMatterParser /// /// /// - FrontMatter? ParseFrontmatterAndMarkdownFromFile(in string filePath, in string sourceContentPath); + IFrontMatter? ParseFrontmatterAndMarkdownFromFile(in string filePath, in string sourceContentPath); /// /// Extract the front matter from the content. @@ -21,7 +21,7 @@ public interface IFrontMatterParser /// /// /// - FrontMatter? ParseFrontmatterAndMarkdown(in string filePath, in string fileContent); + IFrontMatter? ParseFrontmatterAndMarkdown(in string filePath, in string fileContent); /// /// Parse the app config file. diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 9804569..80b0c52 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -12,7 +12,7 @@ namespace SuCoS.Parser; /// /// Responsible for parsing the content front matter using YAML /// -public class YAMLParser : IFrontMatterParser +internal class YAMLParser : IFrontMatterParser { /// /// YamlDotNet parser, strictly set to allow automatically parse only known fields @@ -29,7 +29,7 @@ public class YAMLParser : IFrontMatterParser .Build(); /// - public FrontMatter ParseFrontmatterAndMarkdownFromFile( in string filePath, in string? sourceContentPath = null) + public IFrontMatter ParseFrontmatterAndMarkdownFromFile( in string filePath, in string? sourceContentPath = null) { if (filePath is null) { @@ -52,7 +52,7 @@ public class YAMLParser : IFrontMatterParser } /// - public FrontMatter ParseFrontmatterAndMarkdown(in string fileRelativePath, in string fileContent) + public IFrontMatter ParseFrontmatterAndMarkdown(in string fileRelativePath, in string fileContent) { if (fileRelativePath is null) { @@ -79,7 +79,7 @@ public class YAMLParser : IFrontMatterParser return page; } - private FrontMatter ParseYAML(in string filePath, string yaml, in string rawContent) + private IFrontMatter ParseYAML(in string filePath, string yaml, in string rawContent) { var frontMatter = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing front matter"); var section = SiteHelper.GetSection(filePath); diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index f82a3fb..14d1985 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -15,7 +15,7 @@ namespace SuCoS; /// /// Serve Command will live serve the site and watch any changes. /// -public class ServeCommand : BaseGeneratorCommand, IDisposable +internal class ServeCommand : BaseGeneratorCommand, IDisposable { private const string baseURLDefault = "http://localhost"; private const int portDefault = 1122; @@ -103,9 +103,9 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable handlers = new IServerHandlers[]{ new PingRequests(), - new StaticFileRequest(site.SourceStaticPath), - new StaticFileRequest(site.SourceThemeStaticPath), - new RegisteredPageRequest() + new StaticFileRequest(site.SourceStaticPath, false), + new StaticFileRequest(site.SourceThemeStaticPath, true), + new RegisteredPageRequest(site) }; host = new WebHostBuilder() @@ -194,9 +194,9 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable { foreach (var item in handlers) { - if (item.Check(requestPath, site)) + if (item.Check(requestPath)) { - resultType = await item.Handle(context, serverStartTime); + resultType = await item.Handle(context, requestPath, serverStartTime); break; } } diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs index 1ea37bf..c287887 100644 --- a/source/ServerHandlers/IServerHandlers.cs +++ b/source/ServerHandlers/IServerHandlers.cs @@ -1,28 +1,27 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using SuCoS.Models; namespace SuCoS.ServerHandlers; /// /// Handle server requests /// -public interface IServerHandlers +internal interface IServerHandlers { /// /// Check if the condition is met to handle the request /// /// - /// /// - bool Check(string requestPath, Site site); + bool Check(string requestPath); /// /// Process the request /// /// + /// /// /// - Task Handle(HttpContext context, DateTime serverStartTime); + Task Handle(HttpContext context, string requestPath, DateTime serverStartTime); } diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs index debf5bc..927d701 100644 --- a/source/ServerHandlers/PingRequests.cs +++ b/source/ServerHandlers/PingRequests.cs @@ -1,23 +1,22 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using SuCoS.Models; namespace SuCoS.ServerHandlers; /// /// Return the server startup timestamp as the response /// -public class PingRequests : IServerHandlers +internal class PingRequests : IServerHandlers { /// - public bool Check(string requestPath, Site site) + public bool Check(string requestPath) { return requestPath == "/ping"; } /// - public async Task Handle(HttpContext context, DateTime serverStartTime) + public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) { if (context is null) { @@ -28,4 +27,4 @@ public class PingRequests : IServerHandlers return "ping"; } -} +} diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs index 225425c..61feaea 100644 --- a/source/ServerHandlers/RegisteredPageRequest.cs +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -10,31 +10,33 @@ namespace SuCoS.ServerHandlers; /// /// Return the server startup timestamp as the response /// -public class RegisteredPageRequest : IServerHandlers +internal class RegisteredPageRequest : IServerHandlers { - private Page? page; + readonly ISite site; + + public RegisteredPageRequest(ISite site) + { + this.site = site; + } /// - public bool Check(string requestPath, Site site) + public bool Check(string requestPath) { - if (site is null) - { - throw new ArgumentNullException(nameof(site)); - } if (requestPath is null) { throw new ArgumentNullException(nameof(requestPath)); } - return site.PagesReferences.TryGetValue(requestPath, out page); + return site.PagesReferences.TryGetValue(requestPath, out _); } /// - public async Task Handle(HttpContext context, DateTime serverStartTime) + public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) { if (context is null) { throw new ArgumentNullException(nameof(context)); } + site.PagesReferences.TryGetValue(requestPath, out var page); var content = page!.CompleteContent; content = InjectReloadScript(content); await context.Response.WriteAsync(content); diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs index 7ce9bdf..a2e849a 100644 --- a/source/ServerHandlers/StaticFileRequest.cs +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -3,41 +3,43 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.StaticFiles; -using SuCoS.Models; namespace SuCoS.ServerHandlers; /// /// Check if it is one of the Static files (serve the actual file) /// -public class StaticFileRequest : IServerHandlers +internal class StaticFileRequest : IServerHandlers { private readonly string basePath; - private string? fileAbsolutePath; + private bool inTheme; /// /// Constructor /// /// - public StaticFileRequest(string basePath) + /// + public StaticFileRequest(string basePath, bool inTheme) { this.basePath = basePath; + this.inTheme = inTheme; } /// - public bool Check(string requestPath, Site site) + public bool Check(string requestPath) { if (requestPath is null) { throw new ArgumentNullException(nameof(requestPath)); } - fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); return File.Exists(fileAbsolutePath); } /// - public async Task Handle(HttpContext context, DateTime serverStartTime) + public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) { + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); if (context is null) { throw new ArgumentNullException(nameof(context)); @@ -45,7 +47,7 @@ public class StaticFileRequest : IServerHandlers context.Response.ContentType = GetContentType(fileAbsolutePath!); await using var fileStream = new FileStream(fileAbsolutePath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); await fileStream.CopyToAsync(context.Response.Body); - return "static"; + return inTheme ? "themeSt" : "static"; } /// diff --git a/test/ServerHandlers/PingRequestHandlerTests.cs b/test/ServerHandlers/PingRequestHandlerTests.cs index ec55d50..af5cfb2 100644 --- a/test/ServerHandlers/PingRequestHandlerTests.cs +++ b/test/ServerHandlers/PingRequestHandlerTests.cs @@ -18,7 +18,7 @@ public class PingRequestHandlerTests : TestSetup var pingRequests = new PingRequests(); // Act - await pingRequests.Handle(context, todayDate); + await pingRequests.Handle(context, "ping", todayDate); // Assert stream.Seek(0, SeekOrigin.Begin); @@ -39,7 +39,7 @@ public class PingRequestHandlerTests : TestSetup var pingRequests = new PingRequests(); // Act - var result = pingRequests.Check(requestPath, site); + var result = pingRequests.Check(requestPath); // Assert Assert.Equal(expectedResult, result); diff --git a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs index 98d0a6a..ccf7d53 100644 --- a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs +++ b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using SuCoS.Models; using SuCoS.Models.CommandLineOptions; using SuCoS.ServerHandlers; using Xunit; @@ -13,18 +14,18 @@ public class RegisteredPageRequestHandlerTests : TestSetup public void Check_ReturnsTrueForRegisteredPage(string requestPath, bool exist) { // Arrange - var registeredPageRequest = new RegisteredPageRequest(); var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSitePathCONST06)); site.Options = new GenerateOptions { Source = siteFullPath }; + var registeredPageRequest = new RegisteredPageRequest(site); // Act site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); // Assert - Assert.Equal(exist, registeredPageRequest.Check(requestPath, site)); + Assert.Equal(exist, registeredPageRequest.Check(requestPath)); } [Theory] @@ -33,12 +34,12 @@ public class RegisteredPageRequestHandlerTests : TestSetup public async Task Handle_ReturnsExpectedContent2(string requestPath, string testSitePath, bool contains) { // Arrange - var registeredPageRequest = new RegisteredPageRequest(); var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSitePath)); site.Options = new GenerateOptions { Source = siteFullPath }; + var registeredPageRequest = new RegisteredPageRequest(site); var context = new DefaultHttpContext(); var stream = new MemoryStream(); @@ -46,8 +47,8 @@ public class RegisteredPageRequestHandlerTests : TestSetup // Act site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); - registeredPageRequest.Check(requestPath, site); - await registeredPageRequest.Handle(context, DateTime.Now); + registeredPageRequest.Check(requestPath); + await registeredPageRequest.Handle(context, requestPath, DateTime.Now); // Assert stream.Seek(0, SeekOrigin.Begin); diff --git a/test/ServerHandlers/StaticFileRequestHandlerTests.cs b/test/ServerHandlers/StaticFileRequestHandlerTests.cs index 9824db2..eea246a 100644 --- a/test/ServerHandlers/StaticFileRequestHandlerTests.cs +++ b/test/ServerHandlers/StaticFileRequestHandlerTests.cs @@ -20,13 +20,13 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable { // Arrange var requestPath = Path.GetFileName(tempFilePath); - var basePath = Path.GetDirectoryName(tempFilePath) + var basePath = Path.GetDirectoryName(tempFilePath) ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); - var staticFileRequest = new StaticFileRequest(basePath); + var staticFileRequest = new StaticFileRequest(basePath, false); // Act - var result = staticFileRequest.Check(requestPath, site); + var result = staticFileRequest.Check(requestPath); // Assert Assert.True(result); @@ -37,17 +37,17 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable { // Arrange var requestPath = Path.GetFileName(tempFilePath); - var basePath = Path.GetDirectoryName(tempFilePath) + var basePath = Path.GetDirectoryName(tempFilePath) ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); - var staticFileRequest = new StaticFileRequest(basePath); + var staticFileRequest = new StaticFileRequest(basePath, true); var context = new DefaultHttpContext(); var stream = new MemoryStream(); context.Response.Body = stream; // Act - staticFileRequest.Check(requestPath, site); - await staticFileRequest.Handle(context, DateTime.Now); + staticFileRequest.Check(requestPath); + await staticFileRequest.Handle(context, requestPath, DateTime.Now); // Assert stream.Seek(0, SeekOrigin.Begin); diff --git a/test/TestSetup.cs b/test/TestSetup.cs index acf2fd4..151f2c4 100644 --- a/test/TestSetup.cs +++ b/test/TestSetup.cs @@ -26,13 +26,13 @@ public class TestSetup protected readonly Mock siteSettingsMock = new(); protected readonly Mock loggerMock = new(); protected readonly Mock systemClockMock = new(); - protected readonly FrontMatter frontMatterMock = new() + protected readonly IFrontMatter frontMatterMock = new FrontMatter() { Title = titleCONST, SourcePath = sourcePathCONST }; - protected readonly Site site; + protected readonly ISite site; // based on the compiled test.dll path // that is typically "bin/Debug/netX.0/test.dll" -- GitLab