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;
+}