From 1528a87d10a3d37d9cd1cb7bad0db5e83f9a7106 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 12:59:36 +0000 Subject: [PATCH 1/9] test: Add comprehensive unit tests for ApplicationBuilder, ServiceCollectionExtensions, and ApplicationBuilderExtensions Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com> --- .../ApplicationBuilderExtensionsTests.cs | 269 ++++++++++++++++++ .../ApplicationBuilderTests.cs | 177 +++++++++++- .../ServiceCollectionExtensionsTests.cs | 159 +++++++++++ 3 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs create mode 100644 tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs new file mode 100644 index 0000000..7d70245 --- /dev/null +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs @@ -0,0 +1,269 @@ +namespace NetEvolve.ForgingBlazor.Tests.Unit; + +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.ForgingBlazor.Builders; +using NetEvolve.ForgingBlazor.Extensibility.Abstractions; +using NetEvolve.ForgingBlazor.Extensibility.Models; +using NetEvolve.ForgingBlazor.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..2bf158a 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs @@ -1,4 +1,9 @@ -namespace NetEvolve.ForgingBlazor.Tests.Unit; +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 { @@ -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/ServiceCollectionExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..2ed6b45 --- /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(); + 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(); + 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; +} From f74865219f257f5f2d50e4e2e059ce8d73f7e850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 1 Jan 2026 17:06:56 +0100 Subject: [PATCH 2/9] fix: Discard expression values --- .../ApplicationBuilderExtensionsTests.cs | 14 ++++++------- .../ApplicationBuilderTests.cs | 6 +++--- .../ServiceCollectionExtensionsTests.cs | 20 +++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs index 7d70245..ef46810 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.ForgingBlazor.Tests.Unit; +namespace NetEvolve.ForgingBlazor.Tests.Unit; using Microsoft.Extensions.DependencyInjection; using NetEvolve.ForgingBlazor.Builders; @@ -34,7 +34,7 @@ public async Task AddDefaultContent_RegistersForgingBlazorServices() var args = Array.Empty(); var builder = ApplicationBuilder.CreateEmptyBuilder(args); - builder.AddDefaultContent(); + _ = builder.AddDefaultContent(); var hasContentRegister = builder.Services.Any(x => x.ServiceType == typeof(IContentRegister)); _ = await Assert.That(hasContentRegister).IsTrue(); @@ -46,7 +46,7 @@ public async Task AddDefaultContent_RegistersMarkdownServices() var args = Array.Empty(); var builder = ApplicationBuilder.CreateEmptyBuilder(args); - builder.AddDefaultContent(); + _ = builder.AddDefaultContent(); var hasMarkdownPipeline = builder.Services.Any(x => x.ServiceType == typeof(Markdig.MarkdownPipeline)); _ = await Assert.That(hasMarkdownPipeline).IsTrue(); @@ -58,7 +58,7 @@ public void AddDefaultContent_CalledTwice_ThrowsInvalidOperationException() var args = Array.Empty(); var builder = ApplicationBuilder.CreateEmptyBuilder(args); - builder.AddDefaultContent(); + _ = builder.AddDefaultContent(); _ = Assert.Throws(() => builder.AddDefaultContent()); } @@ -88,7 +88,7 @@ public void AddDefaultContentGeneric_CalledTwice_ThrowsInvalidOperationException var args = Array.Empty(); var builder = ApplicationBuilder.CreateEmptyBuilder(args); - builder.AddDefaultContent(); + _ = builder.AddDefaultContent(); _ = Assert.Throws(() => builder.AddDefaultContent()); } @@ -129,7 +129,7 @@ public async Task AddSegment_RegistersForgingBlazorServices() var args = Array.Empty(); var builder = ApplicationBuilder.CreateEmptyBuilder(args); - builder.AddSegment("test"); + _ = builder.AddSegment("test"); var hasContentRegister = builder.Services.Any(x => x.ServiceType == typeof(IContentRegister)); _ = await Assert.That(hasContentRegister).IsTrue(); @@ -234,7 +234,7 @@ public async Task AddDefaultContent_RegistersContentCollector() var args = Array.Empty(); var builder = ApplicationBuilder.CreateEmptyBuilder(args); - builder.AddDefaultContent(); + _ = builder.AddDefaultContent(); var hasContentCollector = builder.Services.Any(x => x.ServiceType == typeof(IContentCollector)); _ = await Assert.That(hasContentCollector).IsTrue(); diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs index 2bf158a..59b61e3 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.ForgingBlazor.Tests.Unit; +namespace NetEvolve.ForgingBlazor.Tests.Unit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -145,7 +145,7 @@ 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); + _ = builder.Services.AddSingleton(customLoggerFactory); var app = builder.Build(); @@ -158,7 +158,7 @@ public async Task Services_CanBeModified() var args = Array.Empty(); var builder = ApplicationBuilder.CreateDefaultBuilder(args); - builder.Services.AddSingleton(); + _ = builder.Services.AddSingleton(); var hasTestService = builder.Services.Any(x => x.ServiceType == typeof(ITestService)); _ = await Assert.That(hasTestService).IsTrue(); diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs index 2ed6b45..f994aca 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.ForgingBlazor.Tests.Unit; +namespace NetEvolve.ForgingBlazor.Tests.Unit; using System.CommandLine; using Markdig; @@ -33,7 +33,7 @@ public async Task AddForgingBlazorServices_RegistersCommands() { var services = new ServiceCollection(); - services.AddForgingBlazorServices(); + _ = services.AddForgingBlazorServices(); var commandDescriptors = services.Where(x => x.ServiceType == typeof(Command)).ToList(); _ = await Assert.That(commandDescriptors.Count).IsGreaterThanOrEqualTo(4); @@ -44,10 +44,10 @@ public async Task AddForgingBlazorServices_CalledTwice_DoesNotDuplicateServices( { var services = new ServiceCollection(); - services.AddForgingBlazorServices(); + _ = services.AddForgingBlazorServices(); var countAfterFirst = services.Count; - services.AddForgingBlazorServices(); + _ = services.AddForgingBlazorServices(); var countAfterSecond = services.Count; _ = await Assert.That(countAfterFirst).IsEqualTo(countAfterSecond); @@ -77,7 +77,7 @@ public async Task AddMarkdownServices_RegistersYamlDeserializer() { var services = new ServiceCollection(); - services.AddMarkdownServices(); + _ = services.AddMarkdownServices(); var hasDeserializer = services.Any(x => x.ServiceType == typeof(IDeserializer)); _ = await Assert.That(hasDeserializer).IsTrue(); @@ -88,10 +88,10 @@ public async Task AddMarkdownServices_CalledTwice_DoesNotDuplicateServices() { var services = new ServiceCollection(); - services.AddMarkdownServices(); + _ = services.AddMarkdownServices(); var countAfterFirst = services.Count; - services.AddMarkdownServices(); + _ = services.AddMarkdownServices(); var countAfterSecond = services.Count; _ = await Assert.That(countAfterFirst).IsEqualTo(countAfterSecond); @@ -101,7 +101,7 @@ public async Task AddMarkdownServices_CalledTwice_DoesNotDuplicateServices() public async Task IsServiceTypeRegistered_WithRegisteredService_ReturnsTrue() { var services = new ServiceCollection(); - services.AddSingleton(); + _ = services.AddSingleton(); var result = services.IsServiceTypeRegistered(); @@ -133,7 +133,7 @@ public async Task AddMarkdownServices_ConfiguresMarkdownPipelineWithExtensions() { var services = new ServiceCollection(); - services.AddMarkdownServices(); + _ = services.AddMarkdownServices(); using var provider = services.BuildServiceProvider(); var pipeline = provider.GetService(); @@ -145,7 +145,7 @@ public async Task AddMarkdownServices_ConfiguresYamlDeserializerWithCamelCase() { var services = new ServiceCollection(); - services.AddMarkdownServices(); + _ = services.AddMarkdownServices(); using var provider = services.BuildServiceProvider(); var deserializer = provider.GetService(); From e2ba91941737227c89bc15661606d9c76c592676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 1 Jan 2026 17:08:26 +0100 Subject: [PATCH 3/9] fix: Remove unused usings from test file --- .../ApplicationBuilderExtensionsTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs index ef46810..42c269f 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs @@ -1,10 +1,7 @@ namespace NetEvolve.ForgingBlazor.Tests.Unit; -using Microsoft.Extensions.DependencyInjection; -using NetEvolve.ForgingBlazor.Builders; using NetEvolve.ForgingBlazor.Extensibility.Abstractions; using NetEvolve.ForgingBlazor.Extensibility.Models; -using NetEvolve.ForgingBlazor.Models; public sealed class ApplicationBuilderExtensionsTests { From 3f251c9881abda912b967e967a63ae02dd9c760d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 1 Jan 2026 17:16:44 +0100 Subject: [PATCH 4/9] test: Update test signatures and resource disposal patterns - Change test method parameters to nullable strings for segment validation in ApplicationBuilderExtensionsTests. - Use await using for async disposal of service providers in ServiceCollectionExtensionsTests. - Refactor ConfigurationLoaderTests to use unused lambda parameter (_) and update null argument usage. - Minor namespace formatting adjustment (BOM) in ConfigurationLoaderTests. --- .../Configuration/ConfigurationLoaderTests.cs | 2 +- .../ApplicationBuilderExtensionsTests.cs | 8 ++++---- .../Configuration/ConfigurationLoaderTests.cs | 4 ++-- .../ServiceCollectionExtensionsTests.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) 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.Unit/ApplicationBuilderExtensionsTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs index 42c269f..7fb403b 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ApplicationBuilderExtensionsTests.cs @@ -101,7 +101,7 @@ public void AddSegment_WithNullBuilder_ThrowsArgumentNullException() [Arguments(null!)] [Arguments("")] [Arguments(" ")] - public void AddSegment_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string segment) + public void AddSegment_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) { var args = Array.Empty(); var builder = ApplicationBuilder.CreateDefaultBuilder(args); @@ -144,7 +144,7 @@ public void AddSegmentGeneric_WithNullBuilder_ThrowsArgumentNullException() [Arguments(null!)] [Arguments("")] [Arguments(" ")] - public void AddSegmentGeneric_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string segment) + public void AddSegmentGeneric_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) { var args = Array.Empty(); var builder = ApplicationBuilder.CreateDefaultBuilder(args); @@ -175,7 +175,7 @@ public void AddBlogSegment_WithNullBuilder_ThrowsArgumentNullException() [Arguments(null!)] [Arguments("")] [Arguments(" ")] - public void AddBlogSegment_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string segment) + public void AddBlogSegment_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) { var args = Array.Empty(); var builder = ApplicationBuilder.CreateDefaultBuilder(args); @@ -206,7 +206,7 @@ public void AddBlogSegmentGeneric_WithNullBuilder_ThrowsArgumentNullException() [Arguments(null!)] [Arguments("")] [Arguments(" ")] - public void AddBlogSegmentGeneric_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string segment) + public void AddBlogSegmentGeneric_WithNullOrWhiteSpaceSegment_ThrowsArgumentException(string? segment) { var args = Array.Empty(); var builder = ApplicationBuilder.CreateDefaultBuilder(args); 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 index f994aca..f469c41 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Unit/ServiceCollectionExtensionsTests.cs @@ -134,7 +134,7 @@ public async Task AddMarkdownServices_ConfiguresMarkdownPipelineWithExtensions() var services = new ServiceCollection(); _ = services.AddMarkdownServices(); - using var provider = services.BuildServiceProvider(); + await using var provider = services.BuildServiceProvider(); var pipeline = provider.GetService(); _ = await Assert.That(pipeline).IsNotNull(); @@ -146,7 +146,7 @@ public async Task AddMarkdownServices_ConfiguresYamlDeserializerWithCamelCase() var services = new ServiceCollection(); _ = services.AddMarkdownServices(); - using var provider = services.BuildServiceProvider(); + await using var provider = services.BuildServiceProvider(); var deserializer = provider.GetService(); _ = await Assert.That(deserializer).IsNotNull(); From dc1fb1184eb370b03b5ba6919fd3f3b33620c19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 1 Jan 2026 17:28:50 +0100 Subject: [PATCH 5/9] fix: Renamed test configuration files, to match file casing --- ...gingblazor.development.yaml => forgingblazor.Development.yaml} | 0 ...orgingblazor.development.yml => forgingblazor.Development.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YamlOnly/{forgingblazor.development.yaml => forgingblazor.Development.yaml} (100%) rename tests/NetEvolve.ForgingBlazor.Tests.Integration/_setup/YmlOnly/{forgingblazor.development.yml => forgingblazor.Development.yml} (100%) 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 From c372d982884942e4c20c765b807b5320d04cbf93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 1 Jan 2026 17:40:00 +0100 Subject: [PATCH 6/9] fix: Add structured logging to CommandBuild and minor test cleanup - Introduced ILogger to CommandBuild for structured error logging using source generators. - Updated constructor to require a logger instance. - Added LogUnhandledException partial method with [LoggerMessage] for error reporting. - Marked CommandBuild as partial to support source-generated logging. - Simplified argument check in CommandBuildTests using pattern matching. --- .../Commands/CommandBuild.cs | 24 ++++++++++++++++--- .../Commands/CommandBuildTests.cs | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) 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/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs index c5c4d6c..a890151 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs @@ -8,7 +8,7 @@ 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"]; } From a513e27cb7b2be3e65efd83edd9b8f0e2202cad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Fri, 2 Jan 2026 10:48:58 +0100 Subject: [PATCH 7/9] chore: Expanded Logging --- .../Services/ContentRegister.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) 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. /// From 4fba9b6027d21b37bb861e4c32f0c1e91462342d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Fri, 2 Jan 2026 10:51:52 +0100 Subject: [PATCH 8/9] chore: Added logging to Helper Method `VerifyStaticContent` --- tests/NetEvolve.ForgingBlazor.Tests.Integration/Helper.cs | 7 ++++++- .../NetEvolve.ForgingBlazor.Tests.Integration.csproj | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) 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 @@ + From 1f8c98bcc587159a024ad6486b88e5d56bf9a493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Fri, 2 Jan 2026 11:06:41 +0100 Subject: [PATCH 9/9] fix: Case sensitve paths for linux --- .../Commands/CommandBuildTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs index a890151..f268cc4 100644 --- a/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs +++ b/tests/NetEvolve.ForgingBlazor.Tests.Integration/Commands/CommandBuildTests.cs @@ -14,7 +14,7 @@ public async ValueTask Build_DefaultArguments_GeneratesStaticContent(string[] ar } else { - args = ["build", "--content-path", "_setup/Content"]; + args = ["build", "--content-path", "_setup/content"]; } await Helper.VerifyStaticContent(directory.Path, args).ConfigureAwait(false);