diff --git a/src/NetEvolve.ForgingBlazor/Commands/CommandBuild.cs b/src/NetEvolve.ForgingBlazor/Commands/CommandBuild.cs index a2cd2da..12bf088 100644 --- a/src/NetEvolve.ForgingBlazor/Commands/CommandBuild.cs +++ b/src/NetEvolve.ForgingBlazor/Commands/CommandBuild.cs @@ -3,6 +3,7 @@ using System; using System.CommandLine; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using NetEvolve.ForgingBlazor.Configurations; using NetEvolve.ForgingBlazor.Extensibility; using NetEvolve.ForgingBlazor.Extensibility.Abstractions; @@ -22,13 +23,18 @@ /// /// /// -internal sealed class CommandBuild : Command, IStartUpMarker +internal sealed partial class CommandBuild : Command, IStartUpMarker { /// /// Stores the service provider for transferring services during command execution. /// private readonly IServiceProvider _serviceProvider; + /// + /// Stores the logger instance for logging command execution details. + /// + private readonly ILogger _logger; + /// /// Initializes a new instance of the class with the specified service provider. /// @@ -36,10 +42,14 @@ internal sealed class CommandBuild : Command, IStartUpMarker /// The instance providing access to registered application services. /// This is used to transfer services for command execution. /// - public CommandBuild(IServiceProvider serviceProvider) + /// + /// The instance for logging command execution details. + /// + public CommandBuild(IServiceProvider serviceProvider, ILogger logger) : base("build", "Builds and generates static content for a Forging Blazor application.") { _serviceProvider = serviceProvider; + _logger = logger; Add(CommandOptions.ContentPath); Add(CommandOptions.Environment); @@ -111,9 +121,17 @@ private async Task ExecuteAsync(ParseResult parseResult, CancellationToken return 0; } - catch (Exception) + catch (Exception ex) { + LogUnhandledException(ex); return 1; } } + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Error, + Message = "An unhandled exception occurred during the build process." + )] + private partial void LogUnhandledException(Exception ex); } diff --git a/src/NetEvolve.ForgingBlazor/Services/ContentRegister.cs b/src/NetEvolve.ForgingBlazor/Services/ContentRegister.cs index 5b8504a..c289135 100644 --- a/src/NetEvolve.ForgingBlazor/Services/ContentRegister.cs +++ b/src/NetEvolve.ForgingBlazor/Services/ContentRegister.cs @@ -115,12 +115,12 @@ public async ValueTask CollectAsync(CancellationToken cancellationToken) { var collectors = contentCollectors.GetOrAdd( registration.Segment, - (segment) => _serviceProvider.GetKeyedServices(segment) + _serviceProvider.GetKeyedServices ); foreach (var collector in collectors) { - var collectorTypeFullName = collector.GetType().Name; + var collectorTypeFullName = GetCollectorName(collector); LogStartingContentCollection(registration.Segment, collectorTypeFullName); await collector.CollectAsync(this, registration, cancellationToken).ConfigureAwait(false); LogCompletedContentCollection(registration.Segment, collectorTypeFullName); @@ -180,6 +180,28 @@ private static IContentRegistration[] UpdateRegistrations(IContentRegistration[] return [.. registrations.OrderByDescending(r => r.Priority)]; } + private readonly ConcurrentDictionary _collectorNameCache = new(); + + private string GetCollectorName(IContentCollector collector) + { + var collectorType = collector.GetType(); + + return _collectorNameCache.GetOrAdd(collectorType, GetTypeName); + + static string GetTypeName(Type type) + { + if (type.IsGenericType) + { + return type.Name.Split('`')[0] + + "<" + + string.Join(", ", type.GetGenericArguments().Select(x => GetTypeName(x)).ToArray()) + + ">"; + } + + return type.Name; + } + } + /// /// Logs that content collection has started for a specific segment and collector. /// diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs index c5c4d6c..f268cc4 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs @@ -8,13 +8,13 @@ public async ValueTask Build_DefaultArguments_GeneratesStaticContent(string[] ar { using var directory = new TempDirectory(); - if (args is not null && args.Length != 0) + if (args is { Length: > 0 }) { args = [.. args, directory.Path, "--content-path", "_setup/content"]; } else { - args = ["build", "--content-path", "_setup/Content"]; + args = ["build", "--content-path", "_setup/content"]; } await Helper.VerifyStaticContent(directory.Path, args).ConfigureAwait(false); diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Configuration/ConfigurationLoaderTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Configuration/ConfigurationLoaderTests.cs index 7d54ea6..94f7f2a 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Configuration/ConfigurationLoaderTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Configuration/ConfigurationLoaderTests.cs @@ -11,7 +11,7 @@ public sealed class ConfigurationLoaderTests public async ValueTask Load_ConfigurationFile_Expected(string projectPath, string? environment) { var services = new ServiceCollection() - .AddSingleton(sp => ConfigurationLoader.Load(environment, projectPath)) + .AddSingleton(_ => ConfigurationLoader.Load(environment, projectPath)) .ConfigureOptions(); var serviceProvider = services.BuildServiceProvider(); diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Helper.cs b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Helper.cs index bcc9871..a7bfc2a 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Helper.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Helper.cs @@ -1,10 +1,15 @@ namespace NetEvolve.ForgingBlazor.Tests.Integration; +using Microsoft.Extensions.Logging; +using NetEvolve.ForgingBlazor.Logging; + internal static class Helper { public static async ValueTask VerifyStaticContent(string directoryPath, string[] args) { - var builder = ApplicationBuilder.CreateDefaultBuilder(args); + var builder = ApplicationBuilder + .CreateDefaultBuilder(args) + .WithLogging(loggingBuilder => loggingBuilder.AddConsole().SetMinimumLevel(LogLevel.Debug)); var app = builder.Build(); diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Integration/NetEvolve.ForgingBlazor.Tests.Integration.csproj b/tests/NetEvolve.ForgingBlazor.Tests.Integration/NetEvolve.ForgingBlazor.Tests.Integration.csproj index 00e26b1..990a247 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Integration/NetEvolve.ForgingBlazor.Tests.Integration.csproj +++ b/tests/NetEvolve.ForgingBlazor.Tests.Integration/NetEvolve.ForgingBlazor.Tests.Integration.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YamlOnly/forgingblazor.development.yaml b/tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YamlOnly/forgingblazor.Development.yaml similarity index 100% rename from tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YamlOnly/forgingblazor.development.yaml rename to tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YamlOnly/forgingblazor.Development.yaml diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YmlOnly/forgingblazor.development.yml b/tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YmlOnly/forgingblazor.Development.yml similarity index 100% rename from tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YmlOnly/forgingblazor.development.yml rename to tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YmlOnly/forgingblazor.Development.yml diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs new file mode 100644 index 0000000..7fb403b --- /dev/null +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs @@ -0,0 +1,266 @@ +namespace NetEvolve.ForgingBlazor.Tests.Unit; + +using NetEvolve.ForgingBlazor.Extensibility.Abstractions; +using NetEvolve.ForgingBlazor.Extensibility.Models; + +public sealed class ApplicationBuilderExtensionsTests +{ + [Test] + public void AddDefaultContent_WithNullBuilder_ThrowsArgumentNullException() + { + IApplicationBuilder builder = null!; + + _ = Assert.Throws(() => builder.AddDefaultContent()); + } + + [Test] + public async Task AddDefaultContent_RegistersDefaultContentServices() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + var result = builder.AddDefaultContent(); + + _ = await Assert.That(result).IsNotNull(); + _ = await Assert.That(builder.Services.Any(x => x.ServiceType == typeof(IContentRegistration))).IsTrue(); + } + + [Test] + public async Task AddDefaultContent_RegistersForgingBlazorServices() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = builder.AddDefaultContent(); + + var hasContentRegister = builder.Services.Any(x => x.ServiceType == typeof(IContentRegister)); + _ = await Assert.That(hasContentRegister).IsTrue(); + } + + [Test] + public async Task AddDefaultContent_RegistersMarkdownServices() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = builder.AddDefaultContent(); + + var hasMarkdownPipeline = builder.Services.Any(x => x.ServiceType == typeof(Markdig.MarkdownPipeline)); + _ = await Assert.That(hasMarkdownPipeline).IsTrue(); + } + + [Test] + public void AddDefaultContent_CalledTwice_ThrowsInvalidOperationException() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = builder.AddDefaultContent(); + _ = Assert.Throws(() => builder.AddDefaultContent()); + } + + [Test] + public void AddDefaultContentGeneric_WithNullBuilder_ThrowsArgumentNullException() + { + IApplicationBuilder builder = null!; + + _ = Assert.Throws(() => builder.AddDefaultContent()); + } + + [Test] + public async Task AddDefaultContentGeneric_RegistersDefaultContentWithCustomPageType() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + var result = builder.AddDefaultContent(); + + _ = await Assert.That(result).IsNotNull(); + _ = await Assert.That(builder.Services.Any(x => x.ServiceType == typeof(IContentRegistration))).IsTrue(); + } + + [Test] + public void AddDefaultContentGeneric_CalledTwice_ThrowsInvalidOperationException() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = builder.AddDefaultContent(); + _ = Assert.Throws(() => builder.AddDefaultContent()); + } + + [Test] + public void AddSegment_WithNullBuilder_ThrowsArgumentNullException() + { + IApplicationBuilder builder = null!; + + _ = Assert.Throws(() => builder.AddSegment("test")); + } + + [Test] + [Arguments(null!)] + [Arguments("")] + [Arguments(" ")] + public void AddSegment_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + _ = Assert.Throws(() => builder.AddSegment(segment)); + } + + [Test] + public async Task AddSegment_WithValidSegment_ReturnsSegmentBuilder() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var result = builder.AddSegment("blog"); + + _ = await Assert.That(result).IsNotNull(); + } + + [Test] + public async Task AddSegment_RegistersForgingBlazorServices() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = builder.AddSegment("test"); + + var hasContentRegister = builder.Services.Any(x => x.ServiceType == typeof(IContentRegister)); + _ = await Assert.That(hasContentRegister).IsTrue(); + } + + [Test] + public void AddSegmentGeneric_WithNullBuilder_ThrowsArgumentNullException() + { + IApplicationBuilder builder = null!; + + _ = Assert.Throws(() => builder.AddSegment("test")); + } + + [Test] + [Arguments(null!)] + [Arguments("")] + [Arguments(" ")] + public void AddSegmentGeneric_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + _ = Assert.Throws(() => builder.AddSegment(segment)); + } + + [Test] + public async Task AddSegmentGeneric_WithValidSegment_ReturnsSegmentBuilder() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var result = builder.AddSegment("docs"); + + _ = await Assert.That(result).IsNotNull(); + } + + [Test] + public void AddBlogSegment_WithNullBuilder_ThrowsArgumentNullException() + { + IApplicationBuilder builder = null!; + + _ = Assert.Throws(() => builder.AddBlogSegment("blog")); + } + + [Test] + [Arguments(null!)] + [Arguments("")] + [Arguments(" ")] + public void AddBlogSegment_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + _ = Assert.Throws(() => builder.AddBlogSegment(segment)); + } + + [Test] + public async Task AddBlogSegment_WithValidSegment_ReturnsBlogSegmentBuilder() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var result = builder.AddBlogSegment("blog"); + + _ = await Assert.That(result).IsNotNull(); + } + + [Test] + public void AddBlogSegmentGeneric_WithNullBuilder_ThrowsArgumentNullException() + { + IApplicationBuilder builder = null!; + + _ = Assert.Throws(() => builder.AddBlogSegment("blog")); + } + + [Test] + [Arguments(null!)] + [Arguments("")] + [Arguments(" ")] + public void AddBlogSegmentGeneric_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + _ = Assert.Throws(() => builder.AddBlogSegment(segment)); + } + + [Test] + public async Task AddBlogSegmentGeneric_WithValidSegment_ReturnsBlogSegmentBuilder() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var result = builder.AddBlogSegment("posts"); + + _ = await Assert.That(result).IsNotNull(); + } + + [Test] + public async Task AddDefaultContent_RegistersContentCollector() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = builder.AddDefaultContent(); + + var hasContentCollector = builder.Services.Any(x => x.ServiceType == typeof(IContentCollector)); + _ = await Assert.That(hasContentCollector).IsTrue(); + } + + [Test] + public async Task AddSegment_MultipleSegments_RegistersAll() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var result1 = builder.AddSegment("docs"); + var result2 = builder.AddSegment("guides"); + + _ = await Assert.That(result1).IsNotNull(); + _ = await Assert.That(result2).IsNotNull(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Performance", + "CA1812", + Justification = "Used as type parameter in tests" + )] + private sealed record TestPage : PageBase; + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Performance", + "CA1812", + Justification = "Used as type parameter in tests" + )] + private sealed record TestBlogPost : BlogPostBase; +} diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs index 0b0931f..59b61e3 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs @@ -1,5 +1,10 @@ namespace NetEvolve.ForgingBlazor.Tests.Unit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NetEvolve.ForgingBlazor.Extensibility.Abstractions; +using NetEvolve.ForgingBlazor.Extensibility.Models; + public sealed class ApplicationBuilderTests { [Test] @@ -25,4 +30,174 @@ public async Task CreateEmptyBuilder_EmptyArguments_ReturnsOne() _ = await Assert.That(exitCode).IsEqualTo(1); } + + [Test] + public async Task Constructor_WithArgs_CreatesInstanceWithServices() + { + var args = new[] { "arg1", "arg2" }; + + var builder = new ApplicationBuilder(args); + + _ = await Assert.That(builder.Services).IsNotNull(); + } + + [Test] + public async Task CreateDefaultBuilder_WithArgs_ReturnsBuilderWithServices() + { + var args = new[] { "test" }; + + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + _ = await Assert.That(builder).IsNotNull(); + _ = await Assert.That(builder.Services).IsNotNull(); + } + + [Test] + public async Task CreateDefaultBuilder_RegistersDefaultServices() + { + var args = Array.Empty(); + + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var hasContentRegistration = builder.Services.Any(x => x.ServiceType == typeof(IContentRegistration)); + _ = await Assert.That(hasContentRegistration).IsTrue(); + } + + [Test] + public async Task CreateDefaultBuilderGeneric_WithCustomPageType_ReturnsBuilder() + { + var args = Array.Empty(); + + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + _ = await Assert.That(builder).IsNotNull(); + _ = await Assert.That(builder.Services).IsNotNull(); + } + + [Test] + public async Task CreateDefaultBuilderGeneric_RegistersDefaultServicesWithCustomPageType() + { + var args = Array.Empty(); + + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var hasContentRegistration = builder.Services.Any(x => x.ServiceType == typeof(IContentRegistration)); + _ = await Assert.That(hasContentRegistration).IsTrue(); + } + + [Test] + public async Task CreateEmptyBuilder_WithArgs_ReturnsBuilderWithEmptyServices() + { + var args = new[] { "test" }; + + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = await Assert.That(builder).IsNotNull(); + _ = await Assert.That(builder.Services).IsNotNull(); + _ = await Assert.That(builder.Services.Count).IsEqualTo(0); + } + + [Test] + public void Build_WithoutContentRegistration_ThrowsInvalidOperationException() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args); + + _ = Assert.Throws(() => builder.Build()); + } + + [Test] + public async Task Build_WithContentRegistration_CreatesApplication() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var app = builder.Build(); + + _ = await Assert.That(app).IsNotNull(); + } + + [Test] + public async Task Build_WithEmptyBuilderAndDefaultContent_CreatesApplication() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args).AddDefaultContent(); + + var app = builder.Build(); + + _ = await Assert.That(app).IsNotNull(); + } + + [Test] + public async Task Build_AddsNullLoggerWhenNotRegistered() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateEmptyBuilder(args).AddDefaultContent(); + + var app = builder.Build(); + + _ = await Assert.That(app).IsNotNull(); + } + + [Test] + public async Task Build_PreservesExistingLoggerFactory() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + using var customLoggerFactory = LoggerFactory.Create(b => b.AddConsole()); + _ = builder.Services.AddSingleton(customLoggerFactory); + + var app = builder.Build(); + + _ = await Assert.That(app).IsNotNull(); + } + + [Test] + public async Task Services_CanBeModified() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + _ = builder.Services.AddSingleton(); + + var hasTestService = builder.Services.Any(x => x.ServiceType == typeof(ITestService)); + _ = await Assert.That(hasTestService).IsTrue(); + } + + [Test] + public async Task Build_CreatesApplicationWithCommandLineArgs() + { + var args = new[] { "build" }; + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var app = builder.Build(); + var exitCode = await app.RunAsync(); + + _ = await Assert.That(app).IsNotNull(); + // The build command without proper context returns 1, not 0 + _ = await Assert.That(exitCode).IsEqualTo(1); + } + + [Test] + public async Task Build_IncludesServiceDescriptorsInProvider() + { + var args = Array.Empty(); + var builder = ApplicationBuilder.CreateDefaultBuilder(args); + + var app = builder.Build(); + + _ = await Assert.That(app).IsNotNull(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Performance", + "CA1812", + Justification = "Used as type parameter in tests" + )] + private sealed record TestPage : PageBase; + + private interface ITestService; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812", Justification = "Used for DI tests")] + private sealed class TestService : ITestService; } diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/Configuration/ConfigurationLoaderTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/Configuration/ConfigurationLoaderTests.cs index 3f04585..17c3ab6 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/Configuration/ConfigurationLoaderTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/Configuration/ConfigurationLoaderTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.ForgingBlazor.Tests.Unit.Configuration; +namespace NetEvolve.ForgingBlazor.Tests.Unit.Configuration; using NetEvolve.ForgingBlazor.Configurations; @@ -9,5 +9,5 @@ public sealed class ConfigurationLoaderTests [Arguments(" ")] [Arguments("")] public void Load_ConfigurationFile_ThrowsArgumentException_WhenProjectPathIsNullOrWhiteSpace(string? projectPath) => - _ = Assert.Throws(() => ConfigurationLoader.Load(null!, projectPath)); + _ = Assert.Throws(() => ConfigurationLoader.Load(null, projectPath)); } diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..f469c41 --- /dev/null +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,159 @@ +namespace NetEvolve.ForgingBlazor.Tests.Unit; + +using System.CommandLine; +using Markdig; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.ForgingBlazor.Extensibility.Abstractions; +using YamlDotNet.Serialization; + +public sealed class ServiceCollectionExtensionsTests +{ + [Test] + public void AddForgingBlazorServices_WithNullServices_ThrowsArgumentNullException() + { + IServiceCollection services = null!; + + _ = Assert.Throws(() => services.AddForgingBlazorServices()); + } + + [Test] + public async Task AddForgingBlazorServices_RegistersCoreServices() + { + var services = new ServiceCollection(); + + var result = services.AddForgingBlazorServices(); + + _ = await Assert.That(result).IsNotNull(); + _ = await Assert.That(services.Any(x => x.ServiceType == typeof(RootCommand))).IsTrue(); + _ = await Assert.That(services.Any(x => x.ServiceType == typeof(IContentRegister))).IsTrue(); + } + + [Test] + public async Task AddForgingBlazorServices_RegistersCommands() + { + var services = new ServiceCollection(); + + _ = services.AddForgingBlazorServices(); + + var commandDescriptors = services.Where(x => x.ServiceType == typeof(Command)).ToList(); + _ = await Assert.That(commandDescriptors.Count).IsGreaterThanOrEqualTo(4); + } + + [Test] + public async Task AddForgingBlazorServices_CalledTwice_DoesNotDuplicateServices() + { + var services = new ServiceCollection(); + + _ = services.AddForgingBlazorServices(); + var countAfterFirst = services.Count; + + _ = services.AddForgingBlazorServices(); + var countAfterSecond = services.Count; + + _ = await Assert.That(countAfterFirst).IsEqualTo(countAfterSecond); + } + + [Test] + public void AddMarkdownServices_WithNullServices_ThrowsArgumentNullException() + { + IServiceCollection services = null!; + + _ = Assert.Throws(() => services.AddMarkdownServices()); + } + + [Test] + public async Task AddMarkdownServices_RegistersMarkdownPipeline() + { + var services = new ServiceCollection(); + + var result = services.AddMarkdownServices(); + + _ = await Assert.That(result).IsNotNull(); + _ = await Assert.That(services.Any(x => x.ServiceType == typeof(MarkdownPipeline))).IsTrue(); + } + + [Test] + public async Task AddMarkdownServices_RegistersYamlDeserializer() + { + var services = new ServiceCollection(); + + _ = services.AddMarkdownServices(); + + var hasDeserializer = services.Any(x => x.ServiceType == typeof(IDeserializer)); + _ = await Assert.That(hasDeserializer).IsTrue(); + } + + [Test] + public async Task AddMarkdownServices_CalledTwice_DoesNotDuplicateServices() + { + var services = new ServiceCollection(); + + _ = services.AddMarkdownServices(); + var countAfterFirst = services.Count; + + _ = services.AddMarkdownServices(); + var countAfterSecond = services.Count; + + _ = await Assert.That(countAfterFirst).IsEqualTo(countAfterSecond); + } + + [Test] + public async Task IsServiceTypeRegistered_WithRegisteredService_ReturnsTrue() + { + var services = new ServiceCollection(); + _ = services.AddSingleton(); + + var result = services.IsServiceTypeRegistered(); + + _ = await Assert.That(result).IsTrue(); + } + + [Test] + public async Task IsServiceTypeRegistered_WithUnregisteredService_ReturnsFalse() + { + var services = new ServiceCollection(); + + var result = services.IsServiceTypeRegistered(); + + _ = await Assert.That(result).IsFalse(); + } + + [Test] + public async Task IsServiceTypeRegistered_WithEmptyCollection_ReturnsFalse() + { + var services = new ServiceCollection(); + + var result = services.IsServiceTypeRegistered(); + + _ = await Assert.That(result).IsFalse(); + } + + [Test] + public async Task AddMarkdownServices_ConfiguresMarkdownPipelineWithExtensions() + { + var services = new ServiceCollection(); + + _ = services.AddMarkdownServices(); + await using var provider = services.BuildServiceProvider(); + var pipeline = provider.GetService(); + + _ = await Assert.That(pipeline).IsNotNull(); + } + + [Test] + public async Task AddMarkdownServices_ConfiguresYamlDeserializerWithCamelCase() + { + var services = new ServiceCollection(); + + _ = services.AddMarkdownServices(); + await using var provider = services.BuildServiceProvider(); + var deserializer = provider.GetService(); + + _ = await Assert.That(deserializer).IsNotNull(); + } + + private interface ITestService; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812", Justification = "Used for DI tests")] + private sealed class TestService : ITestService; +}