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