Add Aspire Extension (#1109)

* WireMock.Net.Aspire

* .

* xxx

* nuget

* [CodeFactor] Apply fixes

* ut

* t

* **WireMock.Net.Aspire**

* .

* t

* .

* .

* .

* TESTS

* docker utils

* Install .NET Aspire workload

* 4

* 4!

* projects: '**/test/**/*.csproj'

* script: 'dotnet workload install aspire'

* projects: '**/test/**/*.csproj'

* coverage

* WithWatchStaticMappings

* Admin

* typo

* port

* fix

* .

* x

* ...

* wait

* readme

* x

* 2

* async

* <Version>0.0.1-preview-03</Version>

* ...

* fix aspire

* admin/pwd

* Install .NET Aspire workload

* 0.0.1-preview-04

* WaitForHealthAsync

* ...

* IsHealthyAsync

* .

* add eps

* name: 'Execute Aspire Tests'

* name: Install .NET Aspire workload

* .

* dotnet test

* remove duplicate

* .

* cc

* dotnet tool install --global coverlet.console

* -*

* merge

* /d:sonar.pullrequest.provider=github

* <Version>0.0.1-preview-05</Version>

* // Copyright © WireMock.Net

* .

---------

Co-authored-by: codefactor-io <support@codefactor.io>
This commit is contained in:
Stef Heyenrath
2024-07-27 18:53:59 +02:00
committed by GitHub
parent 69c829fae0
commit 4b12f3419f
70 changed files with 2849 additions and 31 deletions

View File

@@ -0,0 +1,16 @@
using WireMock.Net.Aspire.TestAppHost;
var builder = DistributedApplication.CreateBuilder(args);
var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings");
builder
.AddWireMock("wiremock-service")
.WithAdminUserNameAndPassword($"user-{Guid.NewGuid()}", $"pwd-{Guid.NewGuid()}")
.WithMappingsPath(mappingsPath)
.WithWatchStaticMappings()
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
await builder
.Build()
.RunAsync();

View File

@@ -0,0 +1,36 @@
using WireMock.Client.Builders;
namespace WireMock.Net.Aspire.TestAppHost;
internal class WeatherForecastApiMock
{
public static async Task BuildAsync(AdminApiMappingBuilder builder)
{
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
builder.Given(b => b
.WithRequest(request => request
.UsingGet()
.WithPath("/weatherforecast2")
)
.WithResponse(response => response
.WithHeaders(h => h.Add("Content-Type", "application/json"))
.WithBodyAsJson(() => Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray())
)
);
await builder.BuildAndPostAsync();
}
}
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="WireMockMappings\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
{
"Guid": "173d495f-940e-4b86-a1f4-4f0fc7be8b8b",
"Request": {
"Path": "/weatherforecast",
"Methods": [
"get"
]
},
"Response": {
"BodyAsJson": [
{
"date": "2024-05-24",
"temperatureC": -10,
"summary": "Freezing"
},
{
"date": "2024-05-25",
"temperatureC": 33,
"summary": "Hot"
}
],
"Headers": {
"Content-Type": "application/json"
}
}
}

View File

@@ -0,0 +1,62 @@
// Copyright © WireMock.Net
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace WireMock.Net.Aspire.Tests;
[ExcludeFromCodeCoverage]
internal static class DockerUtils
{
public static bool IsDockerRunningLinuxContainerMode()
{
return IsDockerRunning() && IsLinuxContainerMode();
}
private static bool IsDockerRunning()
{
try
{
var processInfo = new ProcessStartInfo("docker", "info")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
var process = Process.Start(processInfo);
process?.WaitForExit();
return process?.ExitCode == 0;
}
catch (Exception ex)
{
Console.WriteLine($"Error checking Docker status: {ex.Message}");
return false;
}
}
private static bool IsLinuxContainerMode()
{
try
{
var processInfo = new ProcessStartInfo("docker", "version --format '{{.Server.Os}}'")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
var process = Process.Start(processInfo);
var output = process?.StandardOutput.ReadToEnd();
process?.WaitForExit();
return output?.Contains("linux", StringComparison.OrdinalIgnoreCase) == true;
}
catch (Exception ex)
{
Console.WriteLine($"Error checking Docker container mode: {ex.Message}");
return false;
}
}
}

View File

@@ -0,0 +1,75 @@
// Copyright © WireMock.Net
using System.Net.Http.Json;
using FluentAssertions;
using Projects;
using Xunit.Abstractions;
namespace WireMock.Net.Aspire.Tests;
public class IntegrationTests(ITestOutputHelper output)
{
private record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);
[Fact]
public async Task StartAppHostWithWireMockAndCreateHttpClientToCallTheMockedWeatherForecastEndpoint()
{
if (!DockerUtils.IsDockerRunningLinuxContainerMode())
{
output.WriteLine("Docker is not running in Linux container mode. Skipping test.");
return;
}
// Arrange
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
using var httpClient = app.CreateHttpClient("wiremock-service");
// Act 1
var weatherForecasts1 = await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast");
// Assert 1
weatherForecasts1.Should().BeEquivalentTo(new[]
{
new WeatherForecast(new DateOnly(2024, 5, 24), -10, "Freezing"),
new WeatherForecast(new DateOnly(2024, 5, 25), +33, "Hot")
});
// Act 2
var weatherForecasts2 = await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast2");
// Assert 2
weatherForecasts2.Should().HaveCount(5);
}
[Fact]
public async Task StartAppHostWithWireMockAndCreateWireMockAdminClientToCallTheAdminEndpoint()
{
if (!DockerUtils.IsDockerRunningLinuxContainerMode())
{
output.WriteLine("Docker is not running in Linux container mode. Skipping test.");
return;
}
// Arrange
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
var adminClient = app.CreateWireMockAdminClient("wiremock-service");
// Act 1
var settings = await adminClient.GetSettingsAsync();
// Assert 1
settings.Should().NotBeNull();
// Act 2
var mappings = await adminClient.GetMappingsAsync();
// Assert 2
mappings.Should().HaveCount(2);
}
}

View File

@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="8.0.0" />
<PackageReference Include="Codecov" Version="1.13.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\WireMock.Net.Aspire.TestAppHost\WireMock.Net.Aspire.TestAppHost.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Aspire.Hosting.Testing" />
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,134 @@
// Copyright © WireMock.Net
using FluentAssertions;
namespace WireMock.Net.Aspire.Tests;
public class WireMockServerArgumentsTests
{
[Fact]
public void DefaultValues_ShouldBeSetCorrectly()
{
// Arrange & Act
var args = new WireMockServerArguments();
// Assert
args.HttpPort.Should().BeNull();
args.AdminUsername.Should().BeNull();
args.AdminPassword.Should().BeNull();
args.ReadStaticMappings.Should().BeFalse();
args.WithWatchStaticMappings.Should().BeFalse();
args.MappingsPath.Should().BeNull();
}
[Fact]
public void HasBasicAuthentication_ShouldReturnTrue_WhenUsernameAndPasswordAreProvided()
{
// Arrange
var args = new WireMockServerArguments
{
AdminUsername = "admin",
AdminPassword = "password"
};
// Act & Assert
args.HasBasicAuthentication.Should().BeTrue();
}
[Fact]
public void HasBasicAuthentication_ShouldReturnFalse_WhenEitherUsernameOrPasswordIsNotProvided()
{
// Arrange
var argsWithUsernameOnly = new WireMockServerArguments { AdminUsername = "admin" };
var argsWithPasswordOnly = new WireMockServerArguments { AdminPassword = "password" };
// Act & Assert
argsWithUsernameOnly.HasBasicAuthentication.Should().BeFalse();
argsWithPasswordOnly.HasBasicAuthentication.Should().BeFalse();
}
[Fact]
public void GetArgs_WhenReadStaticMappingsIsTrue_ShouldContainReadStaticMappingsTrue()
{
// Arrange
var args = new WireMockServerArguments
{
ReadStaticMappings = true
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().ContainInOrder("--ReadStaticMappings", "true");
}
[Fact]
public void GetArgs_WhenReadStaticMappingsIsFalse_ShouldNotContainReadStaticMappingsTrue()
{
// Arrange
var args = new WireMockServerArguments
{
ReadStaticMappings = false
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().NotContain("--ReadStaticMappings", "true");
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetArgs_WhenWithWatchStaticMappingsIsTrue_ShouldContainWatchStaticMappingsTrue(bool readStaticMappings)
{
// Arrange
var args = new WireMockServerArguments
{
WithWatchStaticMappings = true,
ReadStaticMappings = readStaticMappings
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().ContainInOrder("--ReadStaticMappings", "true", "--WatchStaticMappings", "true", "--WatchStaticMappingsInSubdirectories", "true");
}
[Fact]
public void GetArgs_WhenWithWatchStaticMappingsIsFalse_ShouldNotContainWatchStaticMappingsTrue()
{
// Arrange
var args = new WireMockServerArguments
{
WithWatchStaticMappings = false
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().NotContain("--WatchStaticMappings", "true").And.NotContain("--WatchStaticMappingsInSubdirectories", "true");
}
[Fact]
public void GetArgs_ShouldIncludeAuthenticationDetails_WhenAuthenticationIsRequired()
{
// Arrange
var args = new WireMockServerArguments
{
AdminUsername = "admin",
AdminPassword = "password"
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().Contain("--AdminUserName", "admin");
commandLineArgs.Should().Contain("--AdminPassword", "password");
}
}

View File

@@ -0,0 +1,96 @@
// 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<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock(name!, 12345);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException()
{
// Arrange
const int invalidPort = -1;
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock("ValidName", invalidPort);
// Assert
act.Should().Throw<ArgumentOutOfRangeException>().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,
WithWatchStaticMappings = false,
MappingsPath = null,
HttpPort = port
});
wiremock.Resource.Annotations.Should().HaveCount(4);
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
{
Image = "sheyenrath/wiremock.net-alpine",
Registry = null,
Tag = "latest"
});
var endpointAnnotation = wiremock.Resource.Annotations.OfType<EndpointAnnotation>().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<EnvironmentCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
}
}

View File

@@ -13,7 +13,6 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<!--https://developercommunity.visualstudio.com/content/problem/26347/unit-tests-fail-with-fileloadexception-newtonsoftj-1.html-->
@@ -70,7 +69,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="System.Threading" Version="4.3.0" />
<PackageReference Include="RestEase" Version="1.5.7" />
@@ -108,7 +106,7 @@
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.5.0" />
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Grpc.Net.Client" Version="2.59.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.60.0" />
<PackageReference Include="Grpc.Tools" Version="2.60.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -132,10 +130,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="cert.pem">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Grpc\greet.proto">
<GrpcServices>Client</GrpcServices>
<GrpcServices>Client</GrpcServices>
</None>
<None Update="responsebody.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View File

@@ -931,7 +931,7 @@ public class WireMockServerProxyTests
//Arrange
var wireMockServerSettings = new WireMockServerSettings
{
Urls = new[] { "http://localhost:9091" },
Urls = new[] { "http://localhost:19091" },
ProxyAndRecordSettings = new ProxyAndRecordSettings
{
Url = "http://postman-echo.com",
@@ -949,13 +949,13 @@ public class WireMockServerProxyTests
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("http://localhost:9091/post"),
RequestUri = new Uri("http://localhost:19091/post"),
Content = new StringContent(requestBody)
};
var request2 = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("http://localhost:9091/post"),
RequestUri = new Uri("http://localhost:19091/post"),
Content = new StringContent(requestBody)
};
server.ResetMappings();