diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx
index ca80886..1719a33 100644
--- a/.docfx/Dockerfile.docfx
+++ b/.docfx/Dockerfile.docfx
@@ -1,4 +1,4 @@
-ARG NGINX_VERSION=1.30.0-alpine
+ARG NGINX_VERSION=1.31.0-alpine
FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base
RUN rm -rf /usr/share/nginx/html/*
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index d014a76..4cd6ee6 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -180,6 +180,28 @@ Internal classes and methods must be validated by exercising the public API that
- Public entry points provide sufficient coverage of internal code paths.
- The internal implementation exists solely as a helper or utility for public-facing functionality.
+## 10. ExcludeFromCodeCoverage Prohibition
+
+**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes:
+
+- Test classes or test methods
+- Production code
+- Configuration code
+- Any other code path
+
+### Rationale
+
+- Excluding code from coverage hides gaps and creates false confidence in test completeness.
+- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics.
+- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed).
+
+### Alternative Approaches
+
+- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path.
+- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage.
+- **Configuration-only code**: Move to configuration files or extract into testable methods.
+- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary.
+
---
description: 'Writing Performance Tests'
applyTo: "tuning/**, **/*Benchmark*.cs"
diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 26a3bea..780c9b9 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -12,6 +12,10 @@ on:
options:
- Debug
- Release
+ run_mac_tests:
+ type: boolean
+ description: Run the macOS test matrix despite the additional cost and runtime.
+ default: false
permissions:
contents: read
@@ -21,6 +25,7 @@ jobs:
name: initialize
runs-on: ubuntu-24.04
outputs:
+ run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }}
run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }}
strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }}
build-switches: ${{ steps.vars.outputs.build-switches }}
@@ -29,6 +34,12 @@ jobs:
name: calculate workflow variables
shell: bash
run: |
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.run_mac_tests }}" == "true" ]]; then
+ echo "run-mac-tests=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "run-mac-tests=false" >> "$GITHUB_OUTPUT"
+ fi
+
if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then
echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT"
echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT"
@@ -108,10 +119,74 @@ jobs:
build: true
download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }}
+ test_mac:
+ if: ${{ needs.init.outputs.run-mac-tests == 'true' }}
+ name: call-test-mac
+ needs: [init, build]
+ strategy:
+ fail-fast: false
+ matrix:
+ arch: [X64, ARM64]
+ configuration: [Debug, Release]
+ uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3
+ with:
+ runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }}
+ projects: test/**/*.csproj
+ configuration: ${{ matrix.configuration }}
+ verbosity-level: normal
+ build-switches: -p:SkipSignAssembly=true
+ restore: true
+ build: true
+ download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }}
+
+ test_qualitygate:
+ if: ${{ always() }}
+ name: test-qualitygate
+ needs: [init, test_linux, test_windows, test_mac]
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Evaluate test results
+ shell: bash
+ env:
+ RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }}
+ TEST_LINUX_RESULT: ${{ needs.test_linux.result }}
+ TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }}
+ TEST_MAC_RESULT: ${{ needs.test_mac.result }}
+ run: |
+ require_success() {
+ local job_name="$1"
+ local job_result="$2"
+
+ if [[ "$job_result" != "success" ]]; then
+ echo "::error::$job_name finished with '$job_result'."
+ exit 1
+ fi
+ }
+
+ require_success_or_skip() {
+ local job_name="$1"
+ local job_enabled="$2"
+ local job_result="$3"
+
+ if [[ "$job_enabled" == "true" ]]; then
+ require_success "$job_name" "$job_result"
+ return
+ fi
+
+ if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then
+ echo "::error::$job_name finished with '$job_result' while disabled."
+ exit 1
+ fi
+ }
+
+ require_success "test_linux" "$TEST_LINUX_RESULT"
+ require_success "test_windows" "$TEST_WINDOWS_RESULT"
+ require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT"
+
sonarcloud:
- if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
+ if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }}
name: call-sonarcloud
- needs: [init, build, test_linux, test_windows]
+ needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3
with:
organization: geekle
@@ -120,18 +195,18 @@ jobs:
secrets: inherit
codecov:
- if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
+ if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }}
name: call-codecov
- needs: [init, build, test_linux, test_windows]
+ needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1
with:
repository: codebeltnet/bootstrapper
secrets: inherit
codeql:
- if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
+ if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }}
name: call-codeql
- needs: [init, build, test_linux, test_windows]
+ needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3
permissions:
security-events: write
@@ -139,7 +214,7 @@ jobs:
deploy:
if: github.event_name != 'pull_request'
name: call-nuget
- needs: [build, pack, test_linux, test_windows, sonarcloud, codecov, codeql]
+ needs: [build, pack, test_qualitygate, sonarcloud, codecov, codeql]
uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3
with:
version: ${{ needs.build.outputs.version }}
diff --git a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt
index 20051f8..184ef75 100644
--- a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.1.0
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.7
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt
index 4a47cbb..4892fbf 100644
--- a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.1.0
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.7
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt
index 67c7620..c59f6a0 100644
--- a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.1.0
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.7
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt
index 1d99889..fc098da 100644
--- a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt
@@ -1,3 +1,13 @@
+Version: 5.1.0
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
+# New Features
+- ADDED UseBootstrapperEnvironmentDefaults extension method on HostApplicationBuilderExtensions to add conventional environment defaults and user secrets for local development,
+- ADDED UseBootstrapperEnvironmentDefaults extension method on HostBuilderExtensions to add user secrets for local development.
+
Version: 5.0.7
Availability: .NET 10 and .NET 9
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4565c7d..19578f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder.
+## [5.1.0] - 2026-05-28
+
+This is a minor release focused on environment configuration defaults for local development, expanded test coverage to 95%, dependency updates, and CI/CD improvements for macOS testing.
+
+### Added
+
+- UseBootstrapperEnvironmentDefaults extension method on IHostApplicationBuilder to add conventional environment defaults and user secrets for local development,
+- UseBootstrapperEnvironmentDefaults extension method on IHostBuilder to add user secrets for local development,
+- Comprehensive test coverage across bootstrapper console, web, and worker modules including program creation, startup configuration, hosted service initialization, and lifetime management.
+
+### Changed
+
+- Microsoft.NET.Test.Sdk bumped from 18.4.0 to 18.6.0,
+- coverlet.collector and coverlet.msbuild bumped from 10.0.0 to 10.0.1,
+- Target framework packages updated to latest available versions for net9 and net10,
+- DocFX base image updated from nginx 1.30.0-alpine to 1.31.0-alpine,
+- CI pipeline enhanced with optional macOS test matrix for X64 and ARM64 architectures across Debug and Release configurations.
+
## [5.0.7] - 2026-04-18
This is a service update that focuses on package dependencies.
@@ -226,7 +244,9 @@ Highlighted features included in this release:
- WorkerProgram class in the Codebelt.Bootstrapper.Worker namespace that is the base entry point of an application responsible for registering its WorkerStartup partner
- WorkerStartup interface in the Codebelt.Bootstrapper.Worker namespace that provides the base class of a conventional based Startup class for a console application
-[Unreleased]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.6...HEAD
+[Unreleased]: https://github.com/codebeltnet/bootstrapper/compare/v5.1.0...HEAD
+[5.1.0]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.7...v5.1.0
+[5.0.7]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.6...v5.0.7
[5.0.6]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.5...v5.0.6
[5.0.5]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.4...v5.0.5
[5.0.4]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.3...v5.0.4
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5b44624..0db70ed 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,30 +3,30 @@
true
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs b/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs
index b7739ac..2883c33 100644
--- a/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs
+++ b/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs
@@ -19,6 +19,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args)
return Host.CreateDefaultBuilder(args)
.UseBootstrapperLifetime()
.UseBootstrapperStartup()
+ .UseBootstrapperEnvironmentDefaults()
.UseConsoleStartup();
}
}
diff --git a/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs b/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs
index 2c8903d..5f7c506 100644
--- a/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs
+++ b/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs
@@ -29,6 +29,7 @@ internal static HostApplicationBuilder CreateHostBuilder(string[] args, Type pro
{
var hb = Host.CreateApplicationBuilder(args);
hb.UseBootstrapperLifetime();
+ hb.UseBootstrapperEnvironmentDefaults();
hb.UseBootstrapperProgram(programType);
hb.UseMinimalConsoleProgram();
return hb;
diff --git a/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs b/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs
index 156b897..63a8d68 100644
--- a/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs
+++ b/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs
@@ -17,6 +17,7 @@ protected static WebApplicationBuilder CreateHostBuilder(string[] args)
{
var hb = WebApplication.CreateBuilder(args);
hb.UseBootstrapperLifetime();
+ hb.UseBootstrapperEnvironmentDefaults();
return hb;
}
}
diff --git a/src/Codebelt.Bootstrapper.Web/WebProgram.cs b/src/Codebelt.Bootstrapper.Web/WebProgram.cs
index cb2c411..8a17252 100644
--- a/src/Codebelt.Bootstrapper.Web/WebProgram.cs
+++ b/src/Codebelt.Bootstrapper.Web/WebProgram.cs
@@ -19,6 +19,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseBootstrapperLifetime()
+ .UseBootstrapperEnvironmentDefaults()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
diff --git a/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs b/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs
index 46c8c23..f5fadd7 100644
--- a/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs
+++ b/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs
@@ -16,6 +16,7 @@ protected static HostApplicationBuilder CreateHostBuilder(string[] args)
{
var hb = Host.CreateApplicationBuilder(args);
hb.UseBootstrapperLifetime();
+ hb.UseBootstrapperEnvironmentDefaults();
return hb;
}
}
diff --git a/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs b/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs
index 38542eb..11548e5 100644
--- a/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs
+++ b/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs
@@ -18,6 +18,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseBootstrapperLifetime()
+ .UseBootstrapperEnvironmentDefaults()
.UseBootstrapperStartup();
}
}
diff --git a/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs b/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs
index b22396d..943dab5 100644
--- a/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs
+++ b/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs
@@ -1,7 +1,10 @@
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
+using System.IO;
+using System.Reflection;
+using Microsoft.Extensions.Configuration;
namespace Codebelt.Bootstrapper
{
@@ -22,5 +25,33 @@ public static IHostApplicationBuilder UseBootstrapperLifetime(this IHostApplicat
hostBuilder.Services.AddSingleton(provider => provider.GetRequiredService() as BootstrapperLifetime);
return hostBuilder;
}
+
+ ///
+ /// Adds conventional environment defaults for local development.
+ ///
+ /// The to configure.
+ /// The same instance of the for chaining.
+ ///
+ /// When the current environment is local development, this method attempts to resolve the application assembly from
+ /// and adds user secrets to . If the application assembly cannot be resolved, the operation is ignored.
+ ///
+ public static IHostApplicationBuilder UseBootstrapperEnvironmentDefaults(this IHostApplicationBuilder hostBuilder)
+ {
+ if (!hostBuilder.Environment.IsLocalDevelopment()) { return hostBuilder; }
+ if (hostBuilder.Environment.ApplicationName is not { Length: > 0 } applicationName) { return hostBuilder; }
+ var reloadOnChange = hostBuilder.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
+
+ try
+ {
+ var appAssembly = Assembly.Load(new AssemblyName(applicationName));
+ hostBuilder.Configuration.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange);
+ }
+ catch (FileNotFoundException)
+ {
+ // The assembly cannot be found, so just skip it.
+ }
+
+ return hostBuilder;
+ }
}
}
diff --git a/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs b/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs
index b1d2475..4440eea 100644
--- a/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs
+++ b/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs
@@ -1,7 +1,10 @@
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
+using System.IO;
+using System.Reflection;
namespace Codebelt.Bootstrapper
{
@@ -37,5 +40,25 @@ public static IHostBuilder UseBootstrapperStartup(this IHostBuilder ho
services.AddSingleton>(new StartupFactory(services, context.Configuration, context.HostingEnvironment));
});
}
+
+ ///
+ /// Adds conventional environment defaults for local development.
+ ///
+ /// The type of the startup class used to resolve user secrets.
+ /// The to configure.
+ /// The same instance of the for chaining.
+ ///
+ /// When the current environment is local development, this method adds user secrets for to the application configuration.
+ ///
+ public static IHostBuilder UseBootstrapperEnvironmentDefaults(this IHostBuilder hostBuilder) where TStartup : StartupRoot
+ {
+ return hostBuilder.ConfigureAppConfiguration((context, builder) =>
+ {
+ var environment = context.HostingEnvironment;
+ if (!environment.IsLocalDevelopment()) { return; }
+ var reloadOnChange = context.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
+ builder.AddUserSecrets(optional: true, reloadOnChange: reloadOnChange);
+ });
+ }
}
}
diff --git a/src/Codebelt.Bootstrapper/HostEnvironmentExtensions.cs b/src/Codebelt.Bootstrapper/HostEnvironmentExtensions.cs
new file mode 100644
index 0000000..1d0bdf0
--- /dev/null
+++ b/src/Codebelt.Bootstrapper/HostEnvironmentExtensions.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.Hosting;
+
+namespace Codebelt.Bootstrapper
+{
+ internal static class HostEnvironmentExtensions
+ {
+ private const string LocalDevelopment = "LocalDevelopment";
+
+ internal static bool IsLocalDevelopment(this IHostEnvironment environment)
+ {
+ return environment.IsEnvironment(LocalDevelopment);
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/NullStartupFactory.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/NullStartupFactory.cs
new file mode 100644
index 0000000..d0b2bbb
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/NullStartupFactory.cs
@@ -0,0 +1,7 @@
+namespace Codebelt.Bootstrapper.Console.Assets
+{
+ public class NullStartupFactory : global::Codebelt.Bootstrapper.IStartupFactory where TStartup : global::Codebelt.Bootstrapper.StartupRoot
+ {
+ public TStartup Instance => null;
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleProgram.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleProgram.cs
new file mode 100644
index 0000000..2c5e19b
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleProgram.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.Hosting;
+
+namespace Codebelt.Bootstrapper.Console.Assets
+{
+ public class TestConsoleProgram : ConsoleProgram
+ {
+ public static IHostBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args);
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestGenericMinimalConsoleProgram.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestGenericMinimalConsoleProgram.cs
new file mode 100644
index 0000000..3d4f000
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestGenericMinimalConsoleProgram.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Hosting;
+
+namespace Codebelt.Bootstrapper.Console.Assets
+{
+ public class TestGenericMinimalConsoleProgram : MinimalConsoleProgram
+ {
+ public static HostApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args);
+
+ public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs
index 34bdcc6..f1b209a 100644
--- a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs
@@ -3,11 +3,14 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Hosting;
namespace Codebelt.Bootstrapper.Console.Assets
{
public class TestMinimalConsoleProgram : MinimalConsoleProgram
{
+ public static HostApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args);
+
public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var logger = serviceProvider.GetRequiredService>();
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs
index 77572d7..2621c32 100644
--- a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs
@@ -223,5 +223,53 @@ public async Task StartAsync_ShouldNotLogFatalError_WhenExceptionOccursAndSuppre
var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore();
Assert.DoesNotContain(loggerStore.Query(), entry => entry.Message.Contains("Fatal error") && entry.Message.Contains("activating"));
}
+
+ [Fact]
+ public async Task StartAsync_ShouldLogUnableToActivateInstance_WhenStartupIsNullAndSuppressStatusMessagesIsFalse()
+ {
+ await using var test = HostTestFactory.Create(services =>
+ {
+ services.AddXunitTestLogging(TestOutput);
+ services.Configure(options =>
+ {
+ options.SuppressStatusMessages = false;
+ });
+ services.AddSingleton>(new NullStartupFactory());
+ }, hb =>
+ {
+ hb.UseBootstrapperLifetime()
+ .UseConsoleStartup();
+ });
+
+ await test.Host.StartAsync();
+ await test.Host.StopAsync();
+
+ var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore();
+ Assert.Contains(loggerStore.Query(), entry => entry.Message.Contains("Unable to activate"));
+ }
+
+ [Fact]
+ public async Task StartAsync_ShouldNotLogUnableToActivateInstance_WhenStartupIsNullAndSuppressStatusMessagesIsTrue()
+ {
+ await using var test = HostTestFactory.Create(services =>
+ {
+ services.AddXunitTestLogging(TestOutput);
+ services.Configure(options =>
+ {
+ options.SuppressStatusMessages = true;
+ });
+ services.AddSingleton>(new NullStartupFactory());
+ }, hb =>
+ {
+ hb.UseBootstrapperLifetime()
+ .UseConsoleStartup();
+ });
+
+ await test.Host.StartAsync();
+ await test.Host.StopAsync();
+
+ var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore();
+ Assert.DoesNotContain(loggerStore.Query(), entry => entry.Message.Contains("Unable to activate"));
+ }
}
}
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleProgramTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleProgramTest.cs
new file mode 100644
index 0000000..448690b
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleProgramTest.cs
@@ -0,0 +1,30 @@
+using System.Linq;
+using Codebelt.Bootstrapper.Console.Assets;
+using Codebelt.Extensions.Xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Codebelt.Bootstrapper.Console
+{
+ public class ConsoleProgramTest : Test
+ {
+ public ConsoleProgramTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void CreateHostBuilder_ShouldRegisterBootstrapperServices()
+ {
+ using var host = TestConsoleProgram.CreateHostBuilderAccessor([]).Build();
+
+ var lifetime = host.Services.GetRequiredService();
+ var startupFactory = host.Services.GetRequiredService>();
+ var hostedServices = host.Services.GetServices().ToList();
+
+ Assert.IsType(lifetime);
+ Assert.IsType>(startupFactory);
+ Assert.Contains(hostedServices, hostedService => hostedService is ConsoleHostedService);
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleStartupTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleStartupTest.cs
new file mode 100644
index 0000000..7233e2e
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleStartupTest.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.IO;
+using Codebelt.Bootstrapper.Console.Assets;
+using Codebelt.Extensions.Xunit;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Hosting.Internal;
+using Xunit;
+
+namespace Codebelt.Bootstrapper.Console
+{
+ public class ConsoleStartupTest : Test
+ {
+ public ConsoleStartupTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void ConfigureConsole_ShouldAllowDefaultNoOpImplementation()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ { "TestKey", "TestValue" }
+ })
+ .Build();
+
+ var environment = new HostingEnvironment
+ {
+ EnvironmentName = Environments.Development,
+ ApplicationName = "TestApp",
+ ContentRootPath = Directory.GetCurrentDirectory()
+ };
+
+ var startup = new TestConsoleStartup(configuration, environment);
+ var serviceProvider = new ServiceCollection().BuildServiceProvider();
+
+ var exception = Record.Exception(() => startup.ConfigureConsole(serviceProvider));
+
+ Assert.Null(exception);
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleProgramBuilderTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleProgramBuilderTest.cs
new file mode 100644
index 0000000..eb00750
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleProgramBuilderTest.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using Codebelt.Bootstrapper.Console.Assets;
+using Codebelt.Extensions.Xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Codebelt.Bootstrapper.Console
+{
+ public class MinimalConsoleProgramBuilderTest : Test
+ {
+ public MinimalConsoleProgramBuilderTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void CreateHostBuilder_ShouldRegisterMinimalConsoleServices()
+ {
+ using var host = TestMinimalConsoleProgram.CreateHostBuilderAccessor([]).Build();
+
+ var lifetime = host.Services.GetRequiredService();
+ var programFactory = host.Services.GetRequiredService();
+ var hostedServices = host.Services.GetServices().ToList();
+
+ Assert.IsType(lifetime);
+ Assert.NotNull(programFactory);
+ Assert.Contains(hostedServices, hostedService => hostedService is MinimalConsoleHostedService);
+ }
+
+ [Fact]
+ public void CreateHostBuilderOfTProgram_ShouldRegisterMinimalConsoleServices()
+ {
+ using var host = TestGenericMinimalConsoleProgram.CreateHostBuilderAccessor([]).Build();
+
+ var programFactory = host.Services.GetRequiredService();
+ var hostedServices = host.Services.GetServices().ToList();
+
+ Assert.NotNull(programFactory);
+ Assert.Contains(hostedServices, hostedService => hostedService is MinimalConsoleHostedService);
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs
index 529553a..bdfed31 100644
--- a/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs
+++ b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs
@@ -57,5 +57,22 @@ public void OnApplicationStoppingCallback_OnApplicationStoppedCallback_ShouldBeI
Assert.True(stopping && stopped);
}
+
+ [Fact]
+ public void Dispose_ShouldDisposeWrappedHostLifetime()
+ {
+ using var test = HostTestFactory.Create(services =>
+ {
+ services.AddXunitTestLoggingOutputHelperAccessor();
+ services.AddXunitTestLogging(TestOutput);
+ }, hb =>
+ {
+ hb.UseBootstrapperLifetime();
+ }, new TestHostFixture());
+
+ var bootstrapperLifetime = Assert.IsType(test.Host.Services.GetRequiredService());
+
+ bootstrapperLifetime.Dispose();
+ }
}
}
diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLogMessagesTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLogMessagesTest.cs
new file mode 100644
index 0000000..8d02feb
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLogMessagesTest.cs
@@ -0,0 +1,31 @@
+using System;
+using Cuemon;
+using Codebelt.Extensions.Xunit;
+using Codebelt.Extensions.Xunit.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace Codebelt.Bootstrapper
+{
+ public class BootstrapperLogMessagesTest : Test
+ {
+ public BootstrapperLogMessagesTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void FatalErrorActivating_ShouldWriteCriticalEntry()
+ {
+ using var services = new ServiceCollection()
+ .AddXunitTestLogging(TestOutput)
+ .BuildServiceProvider();
+
+ var logger = services.GetRequiredService>();
+
+ Decorator.EncloseToExpose(logger, false).FatalErrorActivating("Test.Type", new InvalidOperationException("Boom"));
+
+ Assert.Contains(logger.GetTestStore().Query(), entry => entry.Message.Contains("Fatal error occurred while activating Test.Type."));
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj b/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj
index 8991216..e91ad88 100644
--- a/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj
+++ b/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj
@@ -2,6 +2,7 @@
Codebelt.Bootstrapper
+ dotnet-Codebelt.Bootstrapper.FunctionalTests-279740fe-8d11-4f29-8fa1-972f5a9dc817
diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs
index 7cc3245..feae8ea 100644
--- a/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs
+++ b/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs
@@ -1,20 +1,21 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using Codebelt.Bootstrapper.Assets;
using Codebelt.Extensions.Xunit;
-using Codebelt.Extensions.Xunit.Hosting;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Hosting.Internal;
using Xunit;
-using static System.Net.Mime.MediaTypeNames;
namespace Codebelt.Bootstrapper
{
public class HostApplicationBuilderExtensionsTest : Test
{
+ private static readonly string TestAssemblyName = typeof(TestStartup).Assembly.GetName().Name!;
+
public HostApplicationBuilderExtensionsTest(ITestOutputHelper output) : base(output)
{
}
@@ -31,5 +32,98 @@ public void UseBootstrapperLifetime_ShouldRegisterBootstrapperLifetime()
Assert.NotNull(bootstrapperLifetime);
Assert.IsType(bootstrapperLifetime);
}
+
+ [Fact]
+ public void UseBootstrapperEnvironmentDefaults_ShouldAddUserSecretsSource_WhenEnvironmentIsLocalDevelopment()
+ {
+ var hb = CreateHostApplicationBuilder("LocalDevelopment", TestAssemblyName, new Dictionary
+ {
+ ["hostBuilder:reloadConfigOnChange"] = "false"
+ });
+ var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources);
+
+ var result = hb.UseBootstrapperEnvironmentDefaults();
+
+ Assert.Same(hb, result);
+ var userSecretsSource = Assert.Single(hb.Configuration.Sources.OfType().Where(IsUserSecretsSource));
+ Assert.Equal(initialSecretsSourceCount + 1, CountUserSecretsSources(hb.Configuration.Sources));
+ Assert.False(userSecretsSource.ReloadOnChange);
+ }
+
+ [Fact]
+ public void UseBootstrapperEnvironmentDefaults_ShouldIgnoreMissingApplicationAssembly_WhenEnvironmentIsLocalDevelopment()
+ {
+ var hb = CreateHostApplicationBuilder("LocalDevelopment", "Codebelt.Bootstrapper.Missing.Assembly", null);
+ var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources);
+
+ var exception = Record.Exception(() => hb.UseBootstrapperEnvironmentDefaults());
+
+ Assert.Null(exception);
+ Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources));
+ }
+
+ [Fact]
+ public void UseBootstrapperEnvironmentDefaults_ShouldReturnWithoutAddingUserSecrets_WhenApplicationNameIsEmpty()
+ {
+ var hb = CreateHostApplicationBuilder("LocalDevelopment", TestAssemblyName, null);
+ ((HostingEnvironment)hb.Environment).ApplicationName = string.Empty;
+ var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources);
+
+ var result = hb.UseBootstrapperEnvironmentDefaults();
+
+ Assert.Same(hb, result);
+ Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources));
+ }
+
+ [Fact]
+ public void UseBootstrapperEnvironmentDefaults_ShouldReturnWithoutAddingUserSecrets_WhenApplicationNameIsNull()
+ {
+ var hb = CreateHostApplicationBuilder("LocalDevelopment", TestAssemblyName, null);
+ ((HostingEnvironment)hb.Environment).ApplicationName = null!;
+ var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources);
+
+ var result = hb.UseBootstrapperEnvironmentDefaults();
+
+ Assert.Same(hb, result);
+ Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources));
+ }
+
+ [Fact]
+ public void UseBootstrapperEnvironmentDefaults_ShouldReturnWithoutAddingUserSecrets_WhenEnvironmentIsNotLocalDevelopment()
+ {
+ var hb = CreateHostApplicationBuilder(Environments.Development, TestAssemblyName, null);
+ var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources);
+
+ var result = hb.UseBootstrapperEnvironmentDefaults();
+
+ Assert.Same(hb, result);
+ Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources));
+ }
+
+ private static HostApplicationBuilder CreateHostApplicationBuilder(string environmentName, string applicationName, Dictionary? configuration)
+ {
+ var settings = new HostApplicationBuilderSettings
+ {
+ EnvironmentName = environmentName,
+ ApplicationName = applicationName
+ };
+ var hostBuilder = Host.CreateApplicationBuilder(settings);
+ if (configuration is not null)
+ {
+ hostBuilder.Configuration.AddInMemoryCollection(configuration);
+ }
+
+ return hostBuilder;
+ }
+
+ private static int CountUserSecretsSources(IEnumerable sources)
+ {
+ return sources.OfType().Count(IsUserSecretsSource);
+ }
+
+ private static bool IsUserSecretsSource(FileConfigurationSource source)
+ {
+ return string.Equals(Path.GetFileName(source.Path), "secrets.json", StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs
index 8bb70d6..b3fcec2 100644
--- a/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs
+++ b/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs
@@ -1,6 +1,11 @@
-using Codebelt.Bootstrapper.Assets;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Codebelt.Bootstrapper.Assets;
using Codebelt.Extensions.Xunit;
using Codebelt.Extensions.Xunit.Hosting;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit;
@@ -44,5 +49,53 @@ public void UseBootstrapperStartup_ShouldRegisterStartupFactory()
Assert.NotNull(startupFactory);
Assert.IsType>(startupFactory);
}
+
+ [Fact]
+ public void UseBootstrapperEnvironmentDefaults_ShouldAddUserSecretsProvider_WhenEnvironmentIsLocalDevelopment()
+ {
+ using var host = CreateHost("LocalDevelopment", configureHostBuilder: hb => hb.UseBootstrapperEnvironmentDefaults(), hostConfiguration: new Dictionary
+ {
+ ["hostBuilder:reloadConfigOnChange"] = "false"
+ });
+
+ var userSecretsProvider = Assert.Single(GetConfigurationRoot(host).Providers.OfType().Where(IsUserSecretsProvider));
+
+ Assert.False(userSecretsProvider.Source.ReloadOnChange);
+ }
+
+ [Fact]
+ public void UseBootstrapperEnvironmentDefaults_ShouldNotAddUserSecretsProvider_WhenEnvironmentIsNotLocalDevelopment()
+ {
+ using var host = CreateHost(Environments.Development, configureHostBuilder: hb => hb.UseBootstrapperEnvironmentDefaults());
+
+ Assert.Empty(GetConfigurationRoot(host).Providers.OfType().Where(IsUserSecretsProvider));
+ }
+
+ private static IHost CreateHost(string environmentName, Action? configureHostBuilder = null, Dictionary? hostConfiguration = null)
+ {
+ var hostBuilder = new HostBuilder()
+ .UseEnvironment(environmentName)
+ .ConfigureHostConfiguration(builder =>
+ {
+ if (hostConfiguration is not null)
+ {
+ builder.AddInMemoryCollection(hostConfiguration);
+ }
+ });
+
+ configureHostBuilder?.Invoke(hostBuilder);
+
+ return hostBuilder.Build();
+ }
+
+ private static IConfigurationRoot GetConfigurationRoot(IHost host)
+ {
+ return Assert.IsAssignableFrom(host.Services.GetRequiredService());
+ }
+
+ private static bool IsUserSecretsProvider(FileConfigurationProvider provider)
+ {
+ return string.Equals(Path.GetFileName(provider.Source.Path), "secrets.json", StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestMinimalWebProgram.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestMinimalWebProgram.cs
new file mode 100644
index 0000000..be07474
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestMinimalWebProgram.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Builder;
+
+namespace Codebelt.Bootstrapper.Web.Assets
+{
+ internal class TestMinimalWebProgram : MinimalWebProgram
+ {
+ public static WebApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args);
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestWebProgram.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestWebProgram.cs
new file mode 100644
index 0000000..491049a
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestWebProgram.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.Hosting;
+
+namespace Codebelt.Bootstrapper.Web.Assets
+{
+ internal class TestWebProgram : WebProgram
+ {
+ public static IHostBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args);
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/MinimalWebProgramBuilderTest.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/MinimalWebProgramBuilderTest.cs
new file mode 100644
index 0000000..58f16bd
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/MinimalWebProgramBuilderTest.cs
@@ -0,0 +1,40 @@
+using System.Threading.Tasks;
+using Codebelt.Bootstrapper.Web.Assets;
+using Codebelt.Extensions.Xunit;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Codebelt.Bootstrapper.Web
+{
+ public class MinimalWebProgramBuilderTest : Test
+ {
+ public MinimalWebProgramBuilderTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public async Task CreateHostBuilder_ShouldRegisterBootstrapperLifetime()
+ {
+ var builder = TestMinimalWebProgram.CreateHostBuilderAccessor([]);
+ builder.WebHost.UseTestServer();
+
+ await using var app = builder.Build();
+ app.MapGet("/", () => "Hello World!");
+
+ await app.StartAsync();
+ try
+ {
+ var client = app.GetTestClient();
+
+ Assert.Equal("Hello World!", await client.GetStringAsync("/"));
+ }
+ finally
+ {
+ await app.StopAsync();
+ }
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/WebProgramTest.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/WebProgramTest.cs
new file mode 100644
index 0000000..0ee69d3
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/WebProgramTest.cs
@@ -0,0 +1,37 @@
+using System.Threading.Tasks;
+using Codebelt.Bootstrapper.Web.Assets;
+using Codebelt.Extensions.Xunit;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Codebelt.Bootstrapper.Web
+{
+ public class WebProgramTest : Test
+ {
+ public WebProgramTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public async Task CreateHostBuilder_ShouldRegisterBootstrapperLifetimeAndStartup()
+ {
+ var builder = TestWebProgram.CreateHostBuilderAccessor([]);
+ builder.ConfigureWebHost(webBuilder => webBuilder.UseTestServer());
+
+ using var host = builder.Build();
+ await host.StartAsync();
+ try
+ {
+ var client = host.GetTestClient();
+
+ Assert.Equal("Hello World!", await client.GetStringAsync("/"));
+ }
+ finally
+ {
+ await host.StopAsync();
+ }
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestMinimalWorkerProgram.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestMinimalWorkerProgram.cs
new file mode 100644
index 0000000..c30f100
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestMinimalWorkerProgram.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.Hosting;
+
+namespace Codebelt.Bootstrapper.Worker.Assets
+{
+ internal class TestMinimalWorkerProgram : MinimalWorkerProgram
+ {
+ public static HostApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args);
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestWorkerProgram.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestWorkerProgram.cs
new file mode 100644
index 0000000..f4e4d1a
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestWorkerProgram.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.Hosting;
+
+namespace Codebelt.Bootstrapper.Worker.Assets
+{
+ internal class TestWorkerProgram : WorkerProgram
+ {
+ public static IHostBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args);
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/MinimalWorkerProgramBuilderTest.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/MinimalWorkerProgramBuilderTest.cs
new file mode 100644
index 0000000..2933724
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/MinimalWorkerProgramBuilderTest.cs
@@ -0,0 +1,25 @@
+using Codebelt.Bootstrapper.Worker.Assets;
+using Codebelt.Extensions.Xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Codebelt.Bootstrapper.Worker
+{
+ public class MinimalWorkerProgramBuilderTest : Test
+ {
+ public MinimalWorkerProgramBuilderTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void CreateHostBuilder_ShouldRegisterBootstrapperLifetime()
+ {
+ using var host = TestMinimalWorkerProgram.CreateHostBuilderAccessor([]).Build();
+
+ var lifetime = host.Services.GetRequiredService();
+
+ Assert.IsType(lifetime);
+ }
+ }
+}
diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/WorkerProgramTest.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/WorkerProgramTest.cs
new file mode 100644
index 0000000..a461ffe
--- /dev/null
+++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/WorkerProgramTest.cs
@@ -0,0 +1,27 @@
+using Codebelt.Bootstrapper.Worker.Assets;
+using Codebelt.Extensions.Xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Codebelt.Bootstrapper.Worker
+{
+ public class WorkerProgramTest : Test
+ {
+ public WorkerProgramTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void CreateHostBuilder_ShouldRegisterBootstrapperLifetimeAndStartup()
+ {
+ using var host = TestWorkerProgram.CreateHostBuilderAccessor([]).Build();
+
+ var lifetime = host.Services.GetRequiredService();
+ var startupFactory = host.Services.GetRequiredService>();
+
+ Assert.IsType(lifetime);
+ Assert.IsType>(startupFactory);
+ }
+ }
+}