diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index e98c695a..a20a72d0 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -1,185 +1,176 @@ -variables: - Prerelease: 'ci' - buildId: "1$(Build.BuildId)" - buildProjects: '**/src/**/*.csproj' - -jobs: -- job: Linux_Build_Test_SonarCloud - - pool: - vmImage: 'ubuntu-22.04' - - steps: - - script: | - echo "BuildId = $(buildId)" - displayName: 'Print buildId' - - - task: CmdLine@2 - displayName: 'Install .NET Aspire workload' - inputs: - script: 'dotnet workload install aspire' - - - script: | - dotnet tool install --global dotnet-sonarscanner - dotnet tool install --global dotnet-coverage - displayName: 'Install dotnet tools' - - - script: | - dotnet workload install aspire - displayName: 'Install aspire' - - - task: PowerShell@2 - displayName: "Use JDK17 by default" - inputs: - targetType: 'inline' - script: | - $jdkPath = $env:JAVA_HOME_17_X64 - Write-Host "##vso[task.setvariable variable=JAVA_HOME]$jdkPath" - - - script: | - dotnet dev-certs https --trust || true - displayName: 'dotnet dev-certs https' - - # See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage - - script: | - dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.pullrequest.provider=github /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage-*.xml /d:sonar.verbose=true - displayName: 'Begin analysis on SonarCloud' - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests - - - task: DotNetCoreCLI@2 - displayName: 'Build Unit tests' - inputs: - command: 'build' - projects: '**/test/**/*.csproj' - arguments: '--configuration Debug --framework net8.0' - - - task: CmdLine@2 - inputs: - script: | - dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-xunit.xml" - displayName: 'WireMock.Net.Tests with Coverage' - - - task: CmdLine@2 - inputs: - script: | - dotnet-coverage collect "dotnet test ./test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-tunit.xml" - displayName: 'WireMock.Net.TUnitTests with Coverage' - - - task: CmdLine@2 - inputs: - script: | - dotnet-coverage collect "dotnet test ./test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-middleware.xml" - displayName: 'WireMock.Net.Middleware.Tests with Coverage' - - - task: CmdLine@2 - inputs: - script: | - dotnet-coverage collect "dotnet test ./test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj --configuration Debug --no-build" -f xml -o "wiremock-coverage-aspire.xml" - displayName: 'WireMock.Net.Aspire.Tests with Coverage' - - - task: CmdLine@2 - displayName: 'Merge coverage files' - inputs: - script: 'dotnet coverage merge **/wiremock-coverage-*.xml --output ./test/wiremock-coverage.xml --output-format xml' - - - script: | - dotnet sonarscanner end /d:sonar.token="$(SONAR_TOKEN)" - displayName: 'End analysis on SonarCloud' - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests - - - task: whitesource.ws-bolt.bolt.wss.WhiteSource Bolt@19 - displayName: 'WhiteSource Bolt' - condition: and(succeeded(), eq(variables['RUN_WHITESOURCE'], 'yes')) - - - script: | - bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/wiremock-coverage.xml - displayName: 'Upload coverage results to codecov' - - - task: PublishTestResults@2 - condition: and(succeeded(), eq(variables['PUBLISH_TESTRESULTS'], 'yes')) - inputs: - testRunner: VSTest - testResultsFiles: '**/*.trx' - - - task: PublishBuildArtifacts@1 - displayName: Publish coverage files - inputs: - PathtoPublish: './test/WireMock.Net.Tests/coverage.net8.0.opencover.xml' - -- job: Windows_Build_Test - - pool: - vmImage: 'windows-2022' - - steps: - - task: UseDotNet@2 - displayName: Use .NET 8.0 - inputs: - packageType: 'sdk' - version: '8.0.x' - - - task: DotNetCoreCLI@2 - displayName: 'WireMock.Net.Tests with Coverage' - inputs: - command: 'test' - projects: './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' - arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx' - - - task: DotNetCoreCLI@2 - displayName: 'WireMock.Net.TUnitTests with Coverage' - inputs: - command: 'test' - projects: './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj' - arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx' - - - task: DotNetCoreCLI@2 - displayName: 'WireMock.Net.Middleware.Tests with Coverage' - inputs: - command: 'test' - projects: './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj' - arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx' - -- job: Windows_Release_to_MyGet - dependsOn: Windows_Build_Test - - pool: - vmImage: 'windows-2022' - - steps: - - task: UseDotNet@2 - displayName: Use .NET 8.0 - inputs: - packageType: 'sdk' - version: '8.0.x' - - - task: DotNetCoreCLI@2 - displayName: Build Release - inputs: - command: 'build' - arguments: /p:Configuration=Release - projects: $(buildProjects) - - - task: DotNetCoreCLI@2 - displayName: Pack - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests - inputs: - command: pack - configuration: 'Release' - packagesToPack: $(buildProjects) - nobuild: true - packDirectory: '$(Build.ArtifactStagingDirectory)/packages' - verbosityPack: 'normal' - - - task: PublishBuildArtifacts@1 - displayName: Publish Artifacts - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - - - task: DotNetCoreCLI@2 - displayName: Push to MyGet - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests - inputs: - command: custom - custom: nuget +variables: + Prerelease: 'ci' + buildId: "1$(Build.BuildId)" + buildProjects: '**/src/**/*.csproj' + +jobs: +- job: Linux_Build_Test_SonarCloud + + pool: + vmImage: 'ubuntu-22.04' + + steps: + - script: | + echo "BuildId = $(buildId)" + displayName: 'Print buildId' + + - script: | + dotnet tool install --global dotnet-sonarscanner + dotnet tool install --global dotnet-coverage + displayName: 'Install dotnet tools' + + - task: PowerShell@2 + displayName: "Use JDK17 by default" + inputs: + targetType: 'inline' + script: | + $jdkPath = $env:JAVA_HOME_17_X64 + Write-Host "##vso[task.setvariable variable=JAVA_HOME]$jdkPath" + + - script: | + dotnet dev-certs https --trust || true + displayName: 'dotnet dev-certs https' + + # See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage + - script: | + dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.pullrequest.provider=github /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage-*.xml /d:sonar.verbose=true + displayName: 'Begin analysis on SonarCloud' + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests + + - task: DotNetCoreCLI@2 + displayName: 'Build Unit tests' + inputs: + command: 'build' + projects: '**/test/**/*.csproj' + arguments: '--configuration Debug --framework net8.0' + + - task: CmdLine@2 + inputs: + script: | + dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-xunit.xml" + displayName: 'WireMock.Net.Tests with Coverage' + + - task: CmdLine@2 + inputs: + script: | + dotnet-coverage collect "dotnet test ./test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-tunit.xml" + displayName: 'WireMock.Net.TUnitTests with Coverage' + + - task: CmdLine@2 + inputs: + script: | + dotnet-coverage collect "dotnet test ./test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-middleware.xml" + displayName: 'WireMock.Net.Middleware.Tests with Coverage' + + - task: CmdLine@2 + inputs: + script: | + dotnet-coverage collect "dotnet test ./test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj --configuration Debug --no-build" -f xml -o "wiremock-coverage-aspire.xml" + displayName: 'WireMock.Net.Aspire.Tests with Coverage' + + - task: CmdLine@2 + displayName: 'Merge coverage files' + inputs: + script: 'dotnet coverage merge **/wiremock-coverage-*.xml --output ./test/wiremock-coverage.xml --output-format xml' + + - script: | + dotnet sonarscanner end /d:sonar.token="$(SONAR_TOKEN)" + displayName: 'End analysis on SonarCloud' + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests + + - task: whitesource.ws-bolt.bolt.wss.WhiteSource Bolt@19 + displayName: 'WhiteSource Bolt' + condition: and(succeeded(), eq(variables['RUN_WHITESOURCE'], 'yes')) + + - script: | + bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/wiremock-coverage.xml + displayName: 'Upload coverage results to codecov' + + - task: PublishTestResults@2 + condition: and(succeeded(), eq(variables['PUBLISH_TESTRESULTS'], 'yes')) + inputs: + testRunner: VSTest + testResultsFiles: '**/*.trx' + + - task: PublishBuildArtifacts@1 + displayName: Publish coverage files + inputs: + PathtoPublish: './test/WireMock.Net.Tests/coverage.net8.0.opencover.xml' + +- job: Windows_Build_Test + + pool: + vmImage: 'windows-2022' + + steps: + - task: UseDotNet@2 + displayName: Use .NET 8.0 + inputs: + packageType: 'sdk' + version: '8.0.x' + + - task: DotNetCoreCLI@2 + displayName: 'WireMock.Net.Tests with Coverage' + inputs: + command: 'test' + projects: './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' + arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx' + + - task: DotNetCoreCLI@2 + displayName: 'WireMock.Net.TUnitTests with Coverage' + inputs: + command: 'test' + projects: './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj' + arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx' + + - task: DotNetCoreCLI@2 + displayName: 'WireMock.Net.Middleware.Tests with Coverage' + inputs: + command: 'test' + projects: './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj' + arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx' + +- job: Windows_Release_to_MyGet + dependsOn: Windows_Build_Test + + pool: + vmImage: 'windows-2022' + + steps: + - task: UseDotNet@2 + displayName: Use .NET 8.0 + inputs: + packageType: 'sdk' + version: '8.0.x' + + - task: DotNetCoreCLI@2 + displayName: Build Release + inputs: + command: 'build' + arguments: /p:Configuration=Release + projects: $(buildProjects) + + - task: DotNetCoreCLI@2 + displayName: Pack + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests + inputs: + command: pack + configuration: 'Release' + packagesToPack: $(buildProjects) + nobuild: true + packDirectory: '$(Build.ArtifactStagingDirectory)/packages' + verbosityPack: 'normal' + + - task: PublishBuildArtifacts@1 + displayName: Publish Artifacts + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + + - task: DotNetCoreCLI@2 + displayName: Push to MyGet + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests + inputs: + command: custom + custom: nuget arguments: push $(Build.ArtifactStagingDirectory)\packages\*.nupkg -n -s https://www.myget.org/F/wiremock-net/api/v3/index.json -k $(MyGetKey) \ No newline at end of file diff --git a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj index 086e1522..f0554470 100644 --- a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj +++ b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj @@ -1,23 +1,24 @@ - - - - Exe - net8.0 - enable - enable - true - - - - - - - - - - - - - - - + + + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/examples-Aspire/AspireApp1.AppHostOriginal/AspireApp1.AppHostOriginal.csproj b/examples-Aspire/AspireApp1.AppHostOriginal/AspireApp1.AppHostOriginal.csproj index d31ac8fd..f556db85 100644 --- a/examples-Aspire/AspireApp1.AppHostOriginal/AspireApp1.AppHostOriginal.csproj +++ b/examples-Aspire/AspireApp1.AppHostOriginal/AspireApp1.AppHostOriginal.csproj @@ -1,20 +1,21 @@ - - - - Exe - net8.0 - enable - enable - true - - - - - - - - - - - - + + + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj b/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj index 853ab0a5..7fd91ef2 100644 --- a/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj +++ b/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj @@ -1,49 +1,49 @@ - - - - enable - Aspire extension to start a WireMock.Net server to stub an api. - WireMock.Net.Aspire - Stef Heyenrath - net8.0 - true - WireMock.Net.Aspire - WireMock.Net.Aspire - dotnet;aspire;wiremock;extension - {B6269AAC-170A-4346-8B9A-579DED3D9A12} - true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - true - true - true - ../WireMock.Net/WireMock.Net.ruleset - true - ../WireMock.Net/WireMock.Net.snk - true - MIT - WireMock.Net-LogoAspire.png - ../../resources/WireMock.Net-LogoAspire.ico - - - - - - - - - - - - - true - - - - - - - - - - + + + + enable + Aspire extension to start a WireMock.Net server to stub an api. + WireMock.Net.Aspire + Stef Heyenrath + net8.0 + true + WireMock.Net.Aspire + WireMock.Net.Aspire + dotnet;aspire;wiremock;extension + {B6269AAC-170A-4346-8B9A-579DED3D9A12} + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + ../WireMock.Net/WireMock.Net.ruleset + true + ../WireMock.Net/WireMock.Net.snk + true + MIT + WireMock.Net-LogoAspire.png + ../../resources/WireMock.Net-LogoAspire.ico + + + + + + + + + + + + + true + + + + + + + + + + \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockInspector.cs b/src/WireMock.Net.Aspire/WireMockInspector.cs new file mode 100644 index 00000000..7ea033ef --- /dev/null +++ b/src/WireMock.Net.Aspire/WireMockInspector.cs @@ -0,0 +1,43 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Aspire.Hosting.WireMock; + +internal static class WireMockInspector +{ + /// + /// Opens the WireMockInspector tool to inspect the WireMock server. + /// + /// + /// + /// + /// + /// Copy of + /// without requestFilters and no call to WaitForExit() method in the process so it doesn't block the caller. + /// + public static void Inspect(string wireMockUrl, [CallerMemberName] string title = "") + { + try + { + var arguments = $"attach --adminUrl {wireMockUrl} --autoLoad --instanceName \"{title}\""; + + Process.Start(new ProcessStartInfo + { + FileName = "wiremockinspector", + Arguments = arguments, + UseShellExecute = false + }); + } + catch (Exception e) + { + throw new InvalidOperationException + ( + message: @"Cannot find installation of WireMockInspector. +Execute the following command to install WireMockInspector dotnet tool: +> dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources +To get more info please visit https://github.com/WireMock-Net/WireMockInspector", + innerException: e + ); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs index c5e85b32..bd5df90b 100644 --- a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs +++ b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs @@ -1,175 +1,222 @@ -// Copyright © WireMock.Net - -using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Lifecycle; -using Stef.Validation; -using WireMock.Client.Builders; -using WireMock.Net.Aspire; - -// ReSharper disable once CheckNamespace -namespace Aspire.Hosting; - -/// -/// Provides extension methods for adding WireMock.Net Server resources to the application model. -/// -public static class WireMockServerBuilderExtensions -{ - // Linux only (https://github.com/dotnet/aspire/issues/854) - private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine"; - private const string DefaultLinuxMappingsPath = "/app/__admin/mappings"; - - /// - /// Adds a WireMock.Net Server resource to the application model. - /// - /// The . - /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. - /// The HTTP port for the WireMock Server. - /// A reference to the . - public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null) - { - Guard.NotNull(builder); - Guard.NotNullOrWhiteSpace(name); - Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue); - - return builder.AddWireMock(name, callback => - { - callback.HttpPort = port; - }); - } - - /// - /// Adds a WireMock.Net Server resource to the application model. - /// - /// The . - /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. - /// The arguments to start the WireMock.Net Server. - /// A reference to the . - public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments) - { - Guard.NotNull(builder); - Guard.NotNullOrWhiteSpace(name); - Guard.NotNull(arguments); - - var wireMockContainerResource = new WireMockServerResource(name, arguments); - var resourceBuilder = builder - .AddResource(wireMockContainerResource) - .WithImage(DefaultLinuxImage) - .WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher - .WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort); - - if (!string.IsNullOrEmpty(arguments.MappingsPath)) - { - resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath); - } - - resourceBuilder = resourceBuilder.WithArgs(ctx => - { - foreach (var arg in arguments.GetArgs()) - { - ctx.Args.Add(arg); - } - }); - - return resourceBuilder; - } - - /// - /// Adds a WireMock.Net Server resource to the application model. - /// - /// The . - /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. - /// A callback that allows for setting the . - /// A reference to the . - public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, Action callback) - { - Guard.NotNull(builder); - Guard.NotNullOrWhiteSpace(name); - Guard.NotNull(callback); - - var arguments = new WireMockServerArguments(); - callback(arguments); - - return builder.AddWireMock(name, arguments); - } - - /// - /// Defines if the static mappings should be read at startup. - /// - /// Default set to false. - /// - /// A reference to the . - public static IResourceBuilder WithReadStaticMappings(this IResourceBuilder wiremock) - { - Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true; - return wiremock; - } - - /// - /// Watch the static mapping files + folder for changes when running. - /// - /// Default set to false. - /// - /// A reference to the . - public static IResourceBuilder WithWatchStaticMappings(this IResourceBuilder wiremock) - { - Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true; - return wiremock; - } - - /// - /// Specifies the path for the (static) mapping json files. - /// - /// The . - /// The local path. - /// A reference to the . - public static IResourceBuilder WithMappingsPath(this IResourceBuilder wiremock, string mappingsPath) - { - Guard.NotNullOrWhiteSpace(mappingsPath); - Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath; - - return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath); - } - - /// - /// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP. - /// - /// The . - /// The admin username. - /// The admin password. - /// A reference to the . - public static IResourceBuilder WithAdminUserNameAndPassword(this IResourceBuilder wiremock, string username, string password) - { - Guard.NotNull(wiremock); - - wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username); - wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password); - return wiremock; - } - - /// - /// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource. - /// - /// The . - /// Delegate that will be invoked to configure the WireMock.Net resource. - /// - public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure) - { - return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder)); - } - - /// - /// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource. - /// - /// The . - /// Delegate that will be invoked to configure the WireMock.Net resource. - /// - public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure) - { - Guard.NotNull(wiremock); - - wiremock.ApplicationBuilder.Services.TryAddLifecycleHook(); - wiremock.Resource.Arguments.ApiMappingBuilder = configure; - - return wiremock; - } +// Copyright © WireMock.Net + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Lifecycle; +using Aspire.Hosting.WireMock; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Stef.Validation; +using WireMock.Client.Builders; +using WireMock.Net.Aspire; + +// ReSharper disable once CheckNamespace +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding WireMock.Net Server resources to the application model. +/// +public static class WireMockServerBuilderExtensions +{ + // Linux only (https://github.com/dotnet/aspire/issues/854) + private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine"; + private const string DefaultLinuxMappingsPath = "/app/__admin/mappings"; + + /// + /// Adds a WireMock.Net Server resource to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The HTTP port for the WireMock Server. + /// A reference to the . + public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null) + { + Guard.NotNull(builder); + Guard.NotNullOrWhiteSpace(name); + Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue); + + return builder.AddWireMock(name, callback => + { + callback.HttpPort = port; + }); + } + + /// + /// Adds a WireMock.Net Server resource to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The arguments to start the WireMock.Net Server. + /// A reference to the . + public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments) + { + Guard.NotNull(builder); + Guard.NotNullOrWhiteSpace(name); + Guard.NotNull(arguments); + + var wireMockContainerResource = new WireMockServerResource(name, arguments); + var resourceBuilder = builder + .AddResource(wireMockContainerResource) + .WithImage(DefaultLinuxImage) + .WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher + .WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort) + .WithWireMockInspectorCommand(); + + if (!string.IsNullOrEmpty(arguments.MappingsPath)) + { + resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath); + } + + resourceBuilder = resourceBuilder.WithArgs(ctx => + { + foreach (var arg in arguments.GetArgs()) + { + ctx.Args.Add(arg); + } + }); + + return resourceBuilder; + } + + /// + /// Adds a WireMock.Net Server resource to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// A callback that allows for setting the . + /// A reference to the . + public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, Action callback) + { + Guard.NotNull(builder); + Guard.NotNullOrWhiteSpace(name); + Guard.NotNull(callback); + + var arguments = new WireMockServerArguments(); + callback(arguments); + + return builder.AddWireMock(name, arguments); + } + + /// + /// Defines if the static mappings should be read at startup. + /// + /// Default set to false. + /// + /// A reference to the . + public static IResourceBuilder WithReadStaticMappings(this IResourceBuilder wiremock) + { + Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true; + return wiremock; + } + + /// + /// Watch the static mapping files + folder for changes when running. + /// + /// Default set to false. + /// + /// A reference to the . + public static IResourceBuilder WithWatchStaticMappings(this IResourceBuilder wiremock) + { + Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true; + return wiremock; + } + + /// + /// Specifies the path for the (static) mapping json files. + /// + /// The . + /// The local path. + /// A reference to the . + public static IResourceBuilder WithMappingsPath(this IResourceBuilder wiremock, string mappingsPath) + { + Guard.NotNullOrWhiteSpace(mappingsPath); + Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath; + + return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath); + } + + /// + /// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP. + /// + /// The . + /// The admin username. + /// The admin password. + /// A reference to the . + public static IResourceBuilder WithAdminUserNameAndPassword(this IResourceBuilder wiremock, string username, string password) + { + Guard.NotNull(wiremock); + + wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username); + wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password); + return wiremock; + } + + /// + /// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource. + /// + /// The . + /// Delegate that will be invoked to configure the WireMock.Net resource. + /// + public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure) + { + return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder)); + } + + /// + /// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource. + /// + /// The . + /// Delegate that will be invoked to configure the WireMock.Net resource. + /// + public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure) + { + Guard.NotNull(wiremock); + + wiremock.ApplicationBuilder.Services.TryAddLifecycleHook(); + wiremock.Resource.Arguments.ApiMappingBuilder = configure; + + return wiremock; + } + + /// + /// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting. + /// This requires installation of the WireMockInspector tool. + /// + /// dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources + /// + /// + /// The . + /// + public static IResourceBuilder WithWireMockInspectorCommand(this IResourceBuilder builder) + { + Guard.NotNull(builder); + + CommandOptions commandOptions = new() + { + Description = "Requires installation of the WireMockInspector (https://github.com/WireMock-Net/WireMockInspector) tool:\ndotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources", + UpdateState = OnUpdateResourceState, + IconName = "BoxSearch", + IconVariant = IconVariant.Filled + }; + + builder.WithCommand( + name: "wiremock-inspector", + displayName: "WireMock Inspector", + executeCommand: context => OnRunOpenInspectorCommandAsync(builder), + commandOptions: commandOptions); + + return builder; + } + + private static Task OnRunOpenInspectorCommandAsync(IResourceBuilder builder) + { + WireMockInspector.Inspect(builder.Resource.GetEndpoint().Url); + + return Task.FromResult(CommandResults.Success()); + } + + private static ResourceCommandState OnUpdateResourceState(UpdateCommandStateContext context) + { + return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy + ? ResourceCommandState.Enabled + : ResourceCommandState.Disabled; + } } \ No newline at end of file diff --git a/test/WireMock.Net.Aspire.TestAppHost/WireMock.Net.Aspire.TestAppHost.csproj b/test/WireMock.Net.Aspire.TestAppHost/WireMock.Net.Aspire.TestAppHost.csproj index bd2cf52a..16d5e97c 100644 --- a/test/WireMock.Net.Aspire.TestAppHost/WireMock.Net.Aspire.TestAppHost.csproj +++ b/test/WireMock.Net.Aspire.TestAppHost/WireMock.Net.Aspire.TestAppHost.csproj @@ -1,30 +1,31 @@ - - - - Exe - net8.0 - enable - enable - true - - true - ../../src/WireMock.Net/WireMock.Net.snk - true - - - - - - - - - - - - - - PreserveNewest - - - + + + + + + Exe + net8.0 + enable + enable + + true + ../../src/WireMock.Net/WireMock.Net.snk + true + + + + + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj b/test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj index 246c1c61..51a115cf 100644 --- a/test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj +++ b/test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj @@ -1,46 +1,48 @@ - - - - net8.0 - enable - enable - false - true - true - true - ../../src/WireMock.Net/WireMock.Net.snk - true - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - + + + + net8.0 + enable + enable + false + true + true + true + ../../src/WireMock.Net/WireMock.Net.snk + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs index cad18dea..a2c1f15d 100644 --- a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs +++ b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs @@ -1,96 +1,98 @@ -// Copyright © WireMock.Net - -using System.Net.Sockets; -using FluentAssertions; -using Moq; - -namespace WireMock.Net.Aspire.Tests; - -public class WireMockServerBuilderExtensionsTests -{ - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("\t")] - public void AddWireMock_WithNullOrWhiteSpaceName_ShouldThrowException(string? name) - { - // Arrange - var builder = Mock.Of(); - - // Act - Action act = () => builder.AddWireMock(name!, 12345); - - // Assert - act.Should().Throw(); - } - - [Fact] - public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException() - { - // Arrange - const int invalidPort = -1; - var builder = Mock.Of(); - - // Act - Action act = () => builder.AddWireMock("ValidName", invalidPort); - - // Assert - act.Should().Throw().WithMessage("Specified argument was out of the range of valid values. (Parameter 'port')"); - } - - [Fact] - public void AddWireMock() - { - // Arrange - var name = $"apiservice{Guid.NewGuid()}"; - const int port = 12345; - const string username = "admin"; - const string password = "test"; - var builder = DistributedApplication.CreateBuilder(); - - // Act - var wiremock = builder - .AddWireMock(name, port) - .WithAdminUserNameAndPassword(username, password) - .WithReadStaticMappings(); - - // Assert - wiremock.Resource.Should().NotBeNull(); - wiremock.Resource.Name.Should().Be(name); - wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments - { - AdminPassword = password, - AdminUsername = username, - ReadStaticMappings = true, - WatchStaticMappings = false, - MappingsPath = null, - HttpPort = port - }); - wiremock.Resource.Annotations.Should().HaveCount(4); - - var containerImageAnnotation = wiremock.Resource.Annotations.OfType().FirstOrDefault(); - containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation - { - Image = "sheyenrath/wiremock.net-alpine", - Registry = null, - Tag = "latest" - }); - - var endpointAnnotation = wiremock.Resource.Annotations.OfType().FirstOrDefault(); - endpointAnnotation.Should().BeEquivalentTo(new EndpointAnnotation( - protocol: ProtocolType.Tcp, - uriScheme: "http", - transport: null, - name: null, - port: port, - targetPort: 80, - isExternal: null, - isProxied: true - )); - - wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); - - wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); - } +// Copyright © WireMock.Net + +using System.Net.Sockets; +using FluentAssertions; +using Moq; + +namespace WireMock.Net.Aspire.Tests; + +public class WireMockServerBuilderExtensionsTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] + public void AddWireMock_WithNullOrWhiteSpaceName_ShouldThrowException(string? name) + { + // Arrange + var builder = Mock.Of(); + + // Act + Action act = () => builder.AddWireMock(name!, 12345); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException() + { + // Arrange + const int invalidPort = -1; + var builder = Mock.Of(); + + // Act + Action act = () => builder.AddWireMock("ValidName", invalidPort); + + // Assert + act.Should().Throw().WithMessage("Specified argument was out of the range of valid values. (Parameter 'port')"); + } + + [Fact] + public void AddWireMock() + { + // Arrange + var name = $"apiservice{Guid.NewGuid()}"; + const int port = 12345; + const string username = "admin"; + const string password = "test"; + var builder = DistributedApplication.CreateBuilder(); + + // Act + var wiremock = builder + .AddWireMock(name, port) + .WithAdminUserNameAndPassword(username, password) + .WithReadStaticMappings(); + + // Assert + wiremock.Resource.Should().NotBeNull(); + wiremock.Resource.Name.Should().Be(name); + wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments + { + AdminPassword = password, + AdminUsername = username, + ReadStaticMappings = true, + WatchStaticMappings = false, + MappingsPath = null, + HttpPort = port + }); + wiremock.Resource.Annotations.Should().HaveCount(5); + + var containerImageAnnotation = wiremock.Resource.Annotations.OfType().FirstOrDefault(); + containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation + { + Image = "sheyenrath/wiremock.net-alpine", + Registry = null, + Tag = "latest" + }); + + var endpointAnnotation = wiremock.Resource.Annotations.OfType().FirstOrDefault(); + endpointAnnotation.Should().BeEquivalentTo(new EndpointAnnotation( + protocol: ProtocolType.Tcp, + uriScheme: "http", + transport: null, + name: null, + port: port, + targetPort: 80, + isExternal: null, + isProxied: true + )); + + wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); + + wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); + + wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); + } } \ No newline at end of file