From e739cc766be097e57844d4fbb5af404d1681f057 Mon Sep 17 00:00:00 2001 From: Rafael Freitas Date: Mon, 1 Sep 2025 18:30:42 -0300 Subject: [PATCH 1/4] feat: add Mockaco module with verification support - Add MockacoContainer with GetVerifyAsync method for verification retrieval - Include MockacoVerificationResponse and MockacoHeader classes - Add comprehensive test suite with 16 tests covering all functionality - Add complete module documentation following project standards - Support .NET Standard 2.0/2.1, .NET 8.0/9.0 compatibility The Mockaco module provides HTTP mock server capabilities with verification support for testing HTTP interactions and validating mock behavior in integration tests. --- .github/workflows/cicd.yml | 1 + Testcontainers.sln | 14 + docs/modules/index.md | 1 + docs/modules/mockaco.md | 91 +++++ src/Testcontainers.Mockaco/.editorconfig | 1 + src/Testcontainers.Mockaco/MockacoBuilder.cs | 102 ++++++ .../MockacoConfiguration.cs | 59 +++ .../MockacoContainer.cs | 127 +++++++ .../Testcontainers.Mockaco.csproj | 12 + src/Testcontainers.Mockaco/Usings.cs | 7 + .../MockacoBuilderTest.cs | 67 ++++ .../MockacoContainerTest.cs | 338 ++++++++++++++++++ .../Testcontainers.Mockaco.Tests.csproj | 21 ++ tests/Testcontainers.Mockaco.Tests/Usings.cs | 5 + 14 files changed, 846 insertions(+) create mode 100644 docs/modules/mockaco.md create mode 100644 src/Testcontainers.Mockaco/.editorconfig create mode 100644 src/Testcontainers.Mockaco/MockacoBuilder.cs create mode 100644 src/Testcontainers.Mockaco/MockacoConfiguration.cs create mode 100644 src/Testcontainers.Mockaco/MockacoContainer.cs create mode 100644 src/Testcontainers.Mockaco/Testcontainers.Mockaco.csproj create mode 100644 src/Testcontainers.Mockaco/Usings.cs create mode 100644 tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs create mode 100644 tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs create mode 100644 tests/Testcontainers.Mockaco.Tests/Testcontainers.Mockaco.Tests.csproj create mode 100644 tests/Testcontainers.Mockaco.Tests/Usings.cs diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index e12f947b2..e06500bdd 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -68,6 +68,7 @@ jobs: { name: "Testcontainers.MariaDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Milvus", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Minio", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Mockaco", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.MongoDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.MsSql", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.MySql", runs-on: "ubuntu-22.04" }, diff --git a/Testcontainers.sln b/Testcontainers.sln index e09e2b093..ce82ec65c 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -257,6 +257,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Xunit.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.XunitV3.Tests", "tests\Testcontainers.XunitV3.Tests\Testcontainers.XunitV3.Tests.csproj", "{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Mockaco", "src\Testcontainers.Mockaco\Testcontainers.Mockaco.csproj", "{B723CF67-8A90-428C-BD6F-885E1DA2F5F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Mockaco.Tests", "tests\Testcontainers.Mockaco.Tests\Testcontainers.Mockaco.Tests.csproj", "{E0DEA5DB-4985-4EFB-8824-F561C690F3CF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -751,6 +755,14 @@ Global {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.Build.0 = Release|Any CPU + {B723CF67-8A90-428C-BD6F-885E1DA2F5F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B723CF67-8A90-428C-BD6F-885E1DA2F5F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B723CF67-8A90-428C-BD6F-885E1DA2F5F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B723CF67-8A90-428C-BD6F-885E1DA2F5F4}.Release|Any CPU.Build.0 = Release|Any CPU + {E0DEA5DB-4985-4EFB-8824-F561C690F3CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0DEA5DB-4985-4EFB-8824-F561C690F3CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0DEA5DB-4985-4EFB-8824-F561C690F3CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0DEA5DB-4985-4EFB-8824-F561C690F3CF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -878,5 +890,7 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {B723CF67-8A90-428C-BD6F-885E1DA2F5F4} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {E0DEA5DB-4985-4EFB-8824-F561C690F3CF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/docs/modules/index.md b/docs/modules/index.md index 254a2d9fe..342085141 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -53,6 +53,7 @@ await moduleNameContainer.StartAsync(); | MariaDB | `mariadb:10.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.MariaDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MariaDb) | | Milvus | `milvusdb/milvus:v2.3.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.Milvus) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Milvus) | | MinIO | `minio/minio:RELEASE.2023-01-31T02-24-19Z` | [NuGet](https://www.nuget.org/packages/Testcontainers.Minio) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Minio) | +| Mockaco | `natenho/mockaco:1.9.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Mockaco) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Mockaco) | | MongoDB | `mongo:6.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MongoDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MongoDb) | | MySQL | `mysql:8.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MySql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MySql) | | NATS | `nats:2.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Nats) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Nats) | diff --git a/docs/modules/mockaco.md b/docs/modules/mockaco.md new file mode 100644 index 000000000..62fdc7ac2 --- /dev/null +++ b/docs/modules/mockaco.md @@ -0,0 +1,91 @@ +# Mockaco + +[Mockaco](https://natenho.github.io/Mockaco/) is a HTTP-based API mock server for .NET Core applications. It's designed to be simple, lightweight, and easy to use for testing and development purposes. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.Mockaco +``` + +You can start a Mockaco container instance from any .NET application. This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method. + +=== "Test class" + ```csharp + public sealed class MockacoContainerTest : IAsyncLifetime + { + private readonly MockacoContainer _mockacoContainer = new MockacoBuilder() + .WithTemplatesPath("./templates") + .Build(); + + public async ValueTask InitializeAsync() + { + await _mockacoContainer.StartAsync() + .ConfigureAwait(false); + } + + public async ValueTask DisposeAsync() + { + await _mockacoContainer.DisposeAsync() + .ConfigureAwait(false); + } + } + ``` + +Set up and call a mock endpoint: + +=== "Mock endpoint setup" + ```csharp + [Fact] + public async Task SetupAndCallMockEndpoint() + { + // Given + using var httpClient = new HttpClient(); + var baseUrl = new UriBuilder(Uri.UriSchemeHttp, _mockacoContainer.Hostname, + _mockacoContainer.GetMappedPublicPort(MockacoBuilder.MockacoPort)).Uri; + + // When - Call the mock endpoint + var response = await httpClient.GetAsync(new Uri(baseUrl, "/ping")); + + // Then + Assert.True(response.IsSuccessStatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("pong", content); + } + ``` + +Verify API calls using the verification endpoint: + +=== "Verify API calls" + ```csharp + [Fact] + public async Task VerifyApiCallsWithVerificationEndpoint() + { + // Given + using var httpClient = new HttpClient(); + var baseUrl = new UriBuilder(Uri.UriSchemeHttp, _mockacoContainer.Hostname, + _mockacoContainer.GetMappedPublicPort(MockacoBuilder.MockacoPort)).Uri; + + // When - Call an endpoint and then verify + await httpClient.GetAsync(new Uri(baseUrl, "/ping")); + var verification = await _mockacoContainer.GetVerifyAsync("/ping"); + + // Then + Assert.NotNull(verification); + Assert.Equal("/ping", verification.Route); + Assert.Contains("pong", verification.Body); + } + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" + ```xml + + + + ``` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/_call_out_test_projects.txt" diff --git a/src/Testcontainers.Mockaco/.editorconfig b/src/Testcontainers.Mockaco/.editorconfig new file mode 100644 index 000000000..3567a12e4 --- /dev/null +++ b/src/Testcontainers.Mockaco/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Mockaco/MockacoBuilder.cs b/src/Testcontainers.Mockaco/MockacoBuilder.cs new file mode 100644 index 000000000..3d3c7b3c6 --- /dev/null +++ b/src/Testcontainers.Mockaco/MockacoBuilder.cs @@ -0,0 +1,102 @@ +using System.IO; +using System.Net.Http; + +namespace Testcontainers.Mockaco; + +/// +public sealed class MockacoBuilder : ContainerBuilder +{ + /// + /// The default Docker image used for Mockaco. + /// + public const string MockacoImage = "natenho/mockaco:1.9.9"; + + /// + /// The default port exposed by the Mockaco container. + /// + public const ushort MockacoPort = 5000; + + /// + /// Initializes a new instance of the class + /// with the default . + /// + public MockacoBuilder() : this(new MockacoConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class + /// with the provided . + /// + /// The Docker resource configuration. + private MockacoBuilder(MockacoConfiguration resourceConfiguration) : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + /// Sets the path to the templates directory for the Mockaco container. + /// + /// The absolute path to the templates directory. + /// The updated instance. + public MockacoBuilder WithTemplatesPath(string templatesPath) + { + return Merge(DockerResourceConfiguration, new MockacoConfiguration(templatesPath: templatesPath)) + .WithBindMount(Path.GetFullPath(templatesPath), "/app/Mocks", AccessMode.ReadWrite); + } + + + /// + public override MockacoContainer Build() + { + Validate(); + return new MockacoContainer(DockerResourceConfiguration); + } + + /// + protected override MockacoConfiguration DockerResourceConfiguration { get; } + + /// + protected override MockacoBuilder Init() + { + return base.Init() + .WithImage(MockacoImage) + .WithPortBinding(MockacoPort, true) + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => request + .WithMethod(HttpMethod.Get) + .ForPort(MockacoPort) + .ForPath("_mockaco/ready") + .WithContent(() => new StringContent("Healthy")))); + } + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.TemplatesPath, + nameof(DockerResourceConfiguration.TemplatesPath)) + .NotNull() + .NotEmpty(); + } + + /// + protected override MockacoBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new MockacoConfiguration(resourceConfiguration)); + } + + /// + protected override MockacoBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new MockacoConfiguration(resourceConfiguration)); + } + + /// + protected override MockacoBuilder Merge(MockacoConfiguration oldValue, MockacoConfiguration newValue) + { + return new MockacoBuilder(new MockacoConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Mockaco/MockacoConfiguration.cs b/src/Testcontainers.Mockaco/MockacoConfiguration.cs new file mode 100644 index 000000000..0d075002c --- /dev/null +++ b/src/Testcontainers.Mockaco/MockacoConfiguration.cs @@ -0,0 +1,59 @@ +namespace Testcontainers.Mockaco; + +public class MockacoConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The path wiches contain mock templates. + public MockacoConfiguration(string templatesPath = null) + { + TemplatesPath = templatesPath ?? "/Mocks/Templates"; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MockacoConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MockacoConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public MockacoConfiguration(MockacoConfiguration resourceConfiguration) + : this(new MockacoConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public MockacoConfiguration(MockacoConfiguration oldValue, MockacoConfiguration newValue) + : base(oldValue, newValue) + { + TemplatesPath = BuildConfiguration.Combine(oldValue?.TemplatesPath, newValue?.TemplatesPath); + } + + /// + /// Gets the path to the mock`s template file. + /// + public string TemplatesPath { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.Mockaco/MockacoContainer.cs b/src/Testcontainers.Mockaco/MockacoContainer.cs new file mode 100644 index 000000000..067e85ee2 --- /dev/null +++ b/src/Testcontainers.Mockaco/MockacoContainer.cs @@ -0,0 +1,127 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Testcontainers.Mockaco; + +[PublicAPI] +public class MockacoContainer : DockerContainer +{ + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public MockacoContainer(MockacoConfiguration configuration) : base(configuration) + { + } + + /// + /// Gets the Mockaco endpoint. + /// + /// The Mockaco endpoint. + public string GetEndpoint() + { + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(MockacoBuilder.MockacoPort)).ToString(); + } + + /// + /// Gets the verification data for a specific route. + /// + /// The route to verify. + /// The verification response, or null if not found. + [ItemCanBeNull] + public async Task GetVerifyAsync(string route) + { + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(GetEndpoint()); + + try + { + return await httpClient.GetFromJsonAsync( + $"/_mockaco/verification?route={Uri.EscapeDataString(route)}"); + } + catch (Exception) + { + return null; + } + } + + /// + /// Represents a Mockaco verification response. + /// + public sealed class MockacoVerificationResponse + { + /// + /// Initializes a new instance of the class. + /// + /// The verified route. + /// When the route was called. + /// The request body content. + /// The request headers (optional). + public MockacoVerificationResponse(string route, string timestamp, string body, MockacoHeader[] headers = null) + { + Route = route; + Timestamp = timestamp; + Body = body; + Headers = headers ?? new MockacoHeader[0]; + } + + /// + /// Gets the verified route. + /// + public string Route { get; } + + /// + /// Gets when the route was called. + /// + public string Timestamp { get; } + + /// + /// Gets the request body content. + /// + public string Body { get; } + + /// + /// Gets the request headers. + /// + public MockacoHeader[] Headers { get; } + + /// + /// Deserializes the body to the specified type. + /// + public T GetBodyAs() => JsonSerializer.Deserialize(Body); + + /// + /// Parses the body as JSON. + /// + public JsonDocument GetBodyAsJson() => JsonDocument.Parse(Body); + } + + /// + /// Represents a header in the Mockaco verification response. + /// + public sealed class MockacoHeader + { + /// + /// Initializes a new instance of the class. + /// + /// The header name. + /// The header value. + public MockacoHeader(string key, string value) + { + Key = key; + Value = value; + } + + /// + /// Gets the header name. + /// + public string Key { get; } + + /// + /// Gets the header value. + /// + public string Value { get; } + } +} \ No newline at end of file diff --git a/src/Testcontainers.Mockaco/Testcontainers.Mockaco.csproj b/src/Testcontainers.Mockaco/Testcontainers.Mockaco.csproj new file mode 100644 index 000000000..18ce31aae --- /dev/null +++ b/src/Testcontainers.Mockaco/Testcontainers.Mockaco.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + diff --git a/src/Testcontainers.Mockaco/Usings.cs b/src/Testcontainers.Mockaco/Usings.cs new file mode 100644 index 000000000..3de10419f --- /dev/null +++ b/src/Testcontainers.Mockaco/Usings.cs @@ -0,0 +1,7 @@ +global using System; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; diff --git a/tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs b/tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs new file mode 100644 index 000000000..dd35c8336 --- /dev/null +++ b/tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs @@ -0,0 +1,67 @@ +using DotNet.Testcontainers.Commons; + +namespace Testcontainers.Mockaco.Tests; + +public sealed class MockacoBuilderTest +{ + [Fact] + public void WithTemplatesPath_SetsCorrectPath() + { + // Given + var templatesPath = TestSession.TempDirectoryPath; + + // When + var builder = new MockacoBuilder() + .WithTemplatesPath(templatesPath); + + // Then + var container = builder.Build(); + Assert.NotNull(container); + } + + [Fact] + public void Build_WithoutTemplatesPath_CanBuild() + { + // Given + var builder = new MockacoBuilder(); + + // When + var container = builder.Build(); + + // Then + Assert.NotNull(container); + // Note: Container will use default empty templates path + // This test verifies the builder doesn't throw during build + } + + [Fact] + public void WithTemplatesPath_NonExistentPath_CanBuild() + { + // Given + var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var builder = new MockacoBuilder() + .WithTemplatesPath(nonExistentPath); + + // When + var container = builder.Build(); + + // Then + Assert.NotNull(container); + // Note: Path validation may happen during container start, not build + // This test verifies the builder accepts any path during configuration + } + + [Fact] + public void DefaultImage_IsCorrect() + { + // Given & When & Then + Assert.Equal("natenho/mockaco:1.9.9", MockacoBuilder.MockacoImage); + } + + [Fact] + public void DefaultPort_IsCorrect() + { + // Given & When & Then + Assert.Equal(5000, MockacoBuilder.MockacoPort); + } +} diff --git a/tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs b/tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs new file mode 100644 index 000000000..5154c72e1 --- /dev/null +++ b/tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs @@ -0,0 +1,338 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text; +using DotNet.Testcontainers.Commons; + +namespace Testcontainers.Mockaco.Tests; + +public sealed class MockacoContainerTest : IAsyncLifetime +{ + private static readonly string TemplatesPath = TestSession.TempDirectoryPath; + private readonly MockacoContainer _mockacoContainer; + + public MockacoContainerTest() + { + var pingTemplate = new + { + request = new + { + method = "GET", + route = "ping", + }, + response = new + { + status = "OK", + body = new + { + response = "pong", + }, + }, + }; + + var createUserTemplate = new + { + request = new + { + method = "POST", + route = "users", + }, + response = new + { + status = "Created", + headers = new Dictionary + { + ["Content-Type"] = "application/json", + ["Location"] = "/users/123", + }, + body = new + { + id = 123, + message = "User created successfully", + }, + }, + }; + + var notFoundTemplate = new + { + request = new + { + method = "GET", + route = "notfound", + }, + response = new + { + status = "NotFound", + body = new + { + error = "Resource not found", + }, + }, + }; + + var serverErrorTemplate = new + { + request = new + { + method = "GET", + route = "error", + }, + response = new + { + status = "InternalServerError", + body = new + { + error = "Internal server error", + }, + }, + }; + + var templates = new (object template, string fileName)[] + { + (pingTemplate, "ping.json"), + (createUserTemplate, "create-user.json"), + (notFoundTemplate, "not-found.json"), + (serverErrorTemplate, "server-error.json"), + }; + + foreach (var (template, fileName) in templates) + { + var templateFilePath = Path.Combine(TemplatesPath, fileName); + var templateJson = JsonSerializer.Serialize(template); + File.WriteAllText(templateFilePath, templateJson); + } + + _mockacoContainer = new MockacoBuilder() + .WithTemplatesPath(TemplatesPath) + .Build(); + } + + public async ValueTask InitializeAsync() + { + await _mockacoContainer.StartAsync(); + } + + public ValueTask DisposeAsync() + { + return _mockacoContainer.DisposeAsync(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task GetStatusReturnsHttpStatusCodeOk() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + using var request = new HttpRequestMessage(HttpMethod.Get, "ping"); + + // When + using var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + + // Then + var responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseJson = JsonSerializer.Deserialize>(responseContent); + Assert.Equal("pong", responseJson["response"].ToString()); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task PostRequest_ReturnsCreatedStatus() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + + var requestBody = JsonSerializer.Serialize(new { name = "John Doe", email = "john@example.com" }); + using var request = new HttpRequestMessage(HttpMethod.Post, "users"); + request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + // When + using var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + + // Then + var responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal("/users/123", response.Headers.Location?.ToString()); + + var responseJson = JsonSerializer.Deserialize>(responseContent); + Assert.Equal("123", responseJson["id"].ToString()); + Assert.Equal("User created successfully", responseJson["message"].ToString()); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task NotFoundRequest_Returns404() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + using var request = new HttpRequestMessage(HttpMethod.Get, "notfound"); + + // When + using var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + + // Then + var responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + var responseJson = JsonSerializer.Deserialize>(responseContent); + Assert.Equal("Resource not found", responseJson["error"].ToString()); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ServerErrorRequest_Returns500() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + using var request = new HttpRequestMessage(HttpMethod.Get, "error"); + + // When + using var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + + // Then + var responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var responseJson = JsonSerializer.Deserialize>(responseContent); + Assert.Equal("Internal server error", responseJson["error"].ToString()); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task UnmatchedRequest_ReturnsNotImplemented() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + using var request = new HttpRequestMessage(HttpMethod.Get, "nonexistent-endpoint"); + + // When + using var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + + // Then + var responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode); + Assert.Contains("Incoming request didn't match any mock", responseContent); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task HealthEndpoint_ReturnsHealthy() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + using var request = new HttpRequestMessage(HttpMethod.Get, "_mockaco/ready"); + + // When + using var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + + // Then + Assert.True(response.IsSuccessStatusCode, "Health endpoint should return success"); + + var responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.Equal("Healthy", responseContent); + } + + [Fact] + public void GetEndpoint_ReturnsValidUri() + { + // Given & When + var endpoint = _mockacoContainer.GetEndpoint(); + + // Then + Assert.True(Uri.TryCreate(endpoint, UriKind.Absolute, out var uri), "Endpoint should be a valid URI"); + Assert.Equal("http", uri.Scheme); + Assert.True(uri.Port > 0, "Port should be greater than 0"); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task GetVerifyAsync_AfterRequest_ReturnsVerificationData() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + + var requestBody = JsonSerializer.Serialize(new { name = "John Doe", email = "john@example.com" }); + using var request = new HttpRequestMessage(HttpMethod.Post, "users"); + request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + // When - Make a request first + await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + + // Then - Verify the request was recorded (try different route formats) + var verification = await _mockacoContainer.GetVerifyAsync("/users"); + + Assert.NotNull(verification); + Assert.Equal("/users", verification.Route); + Assert.False(string.IsNullOrEmpty(verification.Timestamp)); + Assert.Contains("John Doe", verification.Body); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task GetVerifyAsync_RouteNotCalled_ReturnsNull() + { + // Given & When + var verification = await _mockacoContainer.GetVerifyAsync("never-called-route"); + + // Then + Assert.Null(verification); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task GetVerifyAsync_GetBodyAs_DeserializesCorrectly() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + + var requestData = new { name = "Jane Doe", email = "jane@example.com", age = 30 }; + var requestBody = JsonSerializer.Serialize(requestData); + using var request = new HttpRequestMessage(HttpMethod.Post, "users"); + request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + // When + await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + var verification = await _mockacoContainer.GetVerifyAsync("/users"); + + // Then + Assert.NotNull(verification); + var deserializedBody = verification.GetBodyAs>(); + Assert.Equal("Jane Doe", deserializedBody["name"].ToString()); + Assert.Equal("jane@example.com", deserializedBody["email"].ToString()); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task GetVerifyAsync_GetBodyAsJson_ParsesCorrectly() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + + var requestBody = JsonSerializer.Serialize(new { id = 123, active = true }); + using var request = new HttpRequestMessage(HttpMethod.Post, "users"); + request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + // When + await httpClient.SendAsync(request, TestContext.Current.CancellationToken); + var verification = await _mockacoContainer.GetVerifyAsync("/users"); + + // Then + Assert.NotNull(verification); + using var jsonDoc = verification.GetBodyAsJson(); + var root = jsonDoc.RootElement; + Assert.Equal(123, root.GetProperty("id").GetInt32()); + Assert.True(root.GetProperty("active").GetBoolean()); + } +} diff --git a/tests/Testcontainers.Mockaco.Tests/Testcontainers.Mockaco.Tests.csproj b/tests/Testcontainers.Mockaco.Tests/Testcontainers.Mockaco.Tests.csproj new file mode 100644 index 000000000..7bd5d3a68 --- /dev/null +++ b/tests/Testcontainers.Mockaco.Tests/Testcontainers.Mockaco.Tests.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + false + false + Exe + + + + + + + + + + + + + + diff --git a/tests/Testcontainers.Mockaco.Tests/Usings.cs b/tests/Testcontainers.Mockaco.Tests/Usings.cs new file mode 100644 index 000000000..e2418a432 --- /dev/null +++ b/tests/Testcontainers.Mockaco.Tests/Usings.cs @@ -0,0 +1,5 @@ +global using System; +global using System.IO; +global using System.Net; +global using System.Threading.Tasks; +global using Xunit; From ef206011051e93edaea60de126b8c5f522f30015 Mon Sep 17 00:00:00 2001 From: Rafael Freitas Date: Mon, 1 Sep 2025 18:53:45 -0300 Subject: [PATCH 2/4] docs: update Mockaco documentation with template creation details and example --- docs/modules/mockaco.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/modules/mockaco.md b/docs/modules/mockaco.md index 62fdc7ac2..b79d79c94 100644 --- a/docs/modules/mockaco.md +++ b/docs/modules/mockaco.md @@ -2,6 +2,31 @@ [Mockaco](https://natenho.github.io/Mockaco/) is a HTTP-based API mock server for .NET Core applications. It's designed to be simple, lightweight, and easy to use for testing and development purposes. +## Prerequisites + +Before using Mockaco, you need to create mock templates (JSON files) that define the API endpoints and their responses. These templates should be placed in a folder that will be mounted to the container. + +Create a template file (e.g., `ping-pong.json`) in your templates folder: + +```json title="./templates/ping-pong.json" +{ + "request": { + "method": "GET", + "route": "ping" + }, + "response": { + "status": "OK", + "body": { + "response": "pong" + } + } +} +``` + +For more information about creating templates, see the [official Mockaco documentation](https://natenho.github.io/Mockaco/docs/quick-start/create-mock). + +## Installation + Add the following dependency to your project file: ```shell title="NuGet" @@ -10,12 +35,14 @@ dotnet add package Testcontainers.Mockaco You can start a Mockaco container instance from any .NET application. This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method. +**Note:** The `WithTemplatesPath()` method specifies the local folder containing your JSON template files, which will be mounted to the container's `/app/Mocks` directory. + === "Test class" ```csharp public sealed class MockacoContainerTest : IAsyncLifetime { private readonly MockacoContainer _mockacoContainer = new MockacoBuilder() - .WithTemplatesPath("./templates") + .WithTemplatesPath("./templates") // Local folder with JSON templates .Build(); public async ValueTask InitializeAsync() From de8b6bff6b01403394f17db5cc06766314a6a659 Mon Sep 17 00:00:00 2001 From: Rafael Freitas Date: Wed, 1 Oct 2025 17:55:13 -0300 Subject: [PATCH 3/4] feat(Mockaco): update default Docker image to version 1.9.14 --- src/Testcontainers.Mockaco/MockacoBuilder.cs | 2 +- tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Testcontainers.Mockaco/MockacoBuilder.cs b/src/Testcontainers.Mockaco/MockacoBuilder.cs index 3d3c7b3c6..ab3abcf55 100644 --- a/src/Testcontainers.Mockaco/MockacoBuilder.cs +++ b/src/Testcontainers.Mockaco/MockacoBuilder.cs @@ -9,7 +9,7 @@ public sealed class MockacoBuilder : ContainerBuilder /// The default Docker image used for Mockaco. /// - public const string MockacoImage = "natenho/mockaco:1.9.9"; + public const string MockacoImage = "natenho/mockaco:1.9.14"; /// /// The default port exposed by the Mockaco container. diff --git a/tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs b/tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs index dd35c8336..d8cca88e5 100644 --- a/tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs +++ b/tests/Testcontainers.Mockaco.Tests/MockacoBuilderTest.cs @@ -55,7 +55,7 @@ public void WithTemplatesPath_NonExistentPath_CanBuild() public void DefaultImage_IsCorrect() { // Given & When & Then - Assert.Equal("natenho/mockaco:1.9.9", MockacoBuilder.MockacoImage); + Assert.Equal("natenho/mockaco:1.9.14", MockacoBuilder.MockacoImage); } [Fact] From da1566b81febfff005379ba2acdccd0533927d5d Mon Sep 17 00:00:00 2001 From: Rafael Freitas Date: Wed, 1 Oct 2025 18:19:20 -0300 Subject: [PATCH 4/4] feat(Mockaco): update docs and refactor endpoint methods --- docs/modules/index.md | 2 +- docs/modules/mockaco.md | 6 +-- .../MockacoConfiguration.cs | 4 +- .../MockacoContainer.cs | 8 ++-- .../MockacoContainerTest.cs | 38 +++++++++---------- 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/docs/modules/index.md b/docs/modules/index.md index dab418051..4ca50d73c 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -53,7 +53,7 @@ await moduleNameContainer.StartAsync(); | MariaDB | `mariadb:10.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.MariaDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MariaDb) | | Milvus | `milvusdb/milvus:v2.3.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.Milvus) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Milvus) | | MinIO | `minio/minio:RELEASE.2023-01-31T02-24-19Z` | [NuGet](https://www.nuget.org/packages/Testcontainers.Minio) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Minio) | -| Mockaco | `natenho/mockaco:1.9.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Mockaco) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Mockaco) | +| Mockaco | `natenho/mockaco:1.9.14` | [NuGet](https://www.nuget.org/packages/Testcontainers.Mockaco) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Mockaco) | | MongoDB | `mongo:6.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MongoDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MongoDb) | | MySQL | `mysql:8.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MySql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MySql) | | NATS | `nats:2.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Nats) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Nats) | diff --git a/docs/modules/mockaco.md b/docs/modules/mockaco.md index b79d79c94..c948fe44e 100644 --- a/docs/modules/mockaco.md +++ b/docs/modules/mockaco.md @@ -68,8 +68,7 @@ Set up and call a mock endpoint: { // Given using var httpClient = new HttpClient(); - var baseUrl = new UriBuilder(Uri.UriSchemeHttp, _mockacoContainer.Hostname, - _mockacoContainer.GetMappedPublicPort(MockacoBuilder.MockacoPort)).Uri; + var baseUrl = new Uri(_mockacoContainer.GetBaseAddress()); // When - Call the mock endpoint var response = await httpClient.GetAsync(new Uri(baseUrl, "/ping")); @@ -90,8 +89,7 @@ Verify API calls using the verification endpoint: { // Given using var httpClient = new HttpClient(); - var baseUrl = new UriBuilder(Uri.UriSchemeHttp, _mockacoContainer.Hostname, - _mockacoContainer.GetMappedPublicPort(MockacoBuilder.MockacoPort)).Uri; + var baseUrl = new Uri(_mockacoContainer.GetBaseAddress()); // When - Call an endpoint and then verify await httpClient.GetAsync(new Uri(baseUrl, "/ping")); diff --git a/src/Testcontainers.Mockaco/MockacoConfiguration.cs b/src/Testcontainers.Mockaco/MockacoConfiguration.cs index 0d075002c..e75d33a6a 100644 --- a/src/Testcontainers.Mockaco/MockacoConfiguration.cs +++ b/src/Testcontainers.Mockaco/MockacoConfiguration.cs @@ -5,7 +5,7 @@ public class MockacoConfiguration : ContainerConfiguration /// /// Initializes a new instance of the class. /// - /// The path wiches contain mock templates. + /// The path which contains mock templates. public MockacoConfiguration(string templatesPath = null) { TemplatesPath = templatesPath ?? "/Mocks/Templates"; @@ -53,7 +53,7 @@ public MockacoConfiguration(MockacoConfiguration oldValue, MockacoConfiguration } /// - /// Gets the path to the mock`s template file. + /// Gets the path to the mock's template files. /// public string TemplatesPath { get; } } \ No newline at end of file diff --git a/src/Testcontainers.Mockaco/MockacoContainer.cs b/src/Testcontainers.Mockaco/MockacoContainer.cs index 067e85ee2..a9a38a50b 100644 --- a/src/Testcontainers.Mockaco/MockacoContainer.cs +++ b/src/Testcontainers.Mockaco/MockacoContainer.cs @@ -17,10 +17,10 @@ public MockacoContainer(MockacoConfiguration configuration) : base(configuration } /// - /// Gets the Mockaco endpoint. + /// Gets the Mockaco base address. /// - /// The Mockaco endpoint. - public string GetEndpoint() + /// The Mockaco base address. + public string GetBaseAddress() { return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(MockacoBuilder.MockacoPort)).ToString(); } @@ -34,7 +34,7 @@ public string GetEndpoint() public async Task GetVerifyAsync(string route) { using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(GetEndpoint()); + httpClient.BaseAddress = new Uri(GetBaseAddress()); try { diff --git a/tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs b/tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs index 5154c72e1..4c451689b 100644 --- a/tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs +++ b/tests/Testcontainers.Mockaco.Tests/MockacoContainerTest.cs @@ -123,7 +123,7 @@ public async Task GetStatusReturnsHttpStatusCodeOk() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); using var request = new HttpRequestMessage(HttpMethod.Get, "ping"); // When @@ -143,7 +143,7 @@ public async Task PostRequest_ReturnsCreatedStatus() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); var requestBody = JsonSerializer.Serialize(new { name = "John Doe", email = "john@example.com" }); using var request = new HttpRequestMessage(HttpMethod.Post, "users"); @@ -169,7 +169,7 @@ public async Task NotFoundRequest_Returns404() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); using var request = new HttpRequestMessage(HttpMethod.Get, "notfound"); // When @@ -189,7 +189,7 @@ public async Task ServerErrorRequest_Returns500() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); using var request = new HttpRequestMessage(HttpMethod.Get, "error"); // When @@ -209,7 +209,7 @@ public async Task UnmatchedRequest_ReturnsNotImplemented() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); using var request = new HttpRequestMessage(HttpMethod.Get, "nonexistent-endpoint"); // When @@ -228,7 +228,7 @@ public async Task HealthEndpoint_ReturnsHealthy() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); using var request = new HttpRequestMessage(HttpMethod.Get, "_mockaco/ready"); // When @@ -242,10 +242,10 @@ public async Task HealthEndpoint_ReturnsHealthy() } [Fact] - public void GetEndpoint_ReturnsValidUri() + public void GetBaseAddress_ReturnsValidUri() { // Given & When - var endpoint = _mockacoContainer.GetEndpoint(); + var endpoint = _mockacoContainer.GetBaseAddress(); // Then Assert.True(Uri.TryCreate(endpoint, UriKind.Absolute, out var uri), "Endpoint should be a valid URI"); @@ -259,18 +259,18 @@ public async Task GetVerifyAsync_AfterRequest_ReturnsVerificationData() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); - + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); + var requestBody = JsonSerializer.Serialize(new { name = "John Doe", email = "john@example.com" }); using var request = new HttpRequestMessage(HttpMethod.Post, "users"); request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); // When - Make a request first await httpClient.SendAsync(request, TestContext.Current.CancellationToken); - + // Then - Verify the request was recorded (try different route formats) var verification = await _mockacoContainer.GetVerifyAsync("/users"); - + Assert.NotNull(verification); Assert.Equal("/users", verification.Route); Assert.False(string.IsNullOrEmpty(verification.Timestamp)); @@ -283,7 +283,7 @@ public async Task GetVerifyAsync_RouteNotCalled_ReturnsNull() { // Given & When var verification = await _mockacoContainer.GetVerifyAsync("never-called-route"); - + // Then Assert.Null(verification); } @@ -294,8 +294,8 @@ public async Task GetVerifyAsync_GetBodyAs_DeserializesCorrectly() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); - + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); + var requestData = new { name = "Jane Doe", email = "jane@example.com", age = 30 }; var requestBody = JsonSerializer.Serialize(requestData); using var request = new HttpRequestMessage(HttpMethod.Post, "users"); @@ -304,7 +304,7 @@ public async Task GetVerifyAsync_GetBodyAs_DeserializesCorrectly() // When await httpClient.SendAsync(request, TestContext.Current.CancellationToken); var verification = await _mockacoContainer.GetVerifyAsync("/users"); - + // Then Assert.NotNull(verification); var deserializedBody = verification.GetBodyAs>(); @@ -318,8 +318,8 @@ public async Task GetVerifyAsync_GetBodyAsJson_ParsesCorrectly() { // Given using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(_mockacoContainer.GetEndpoint()); - + httpClient.BaseAddress = new Uri(_mockacoContainer.GetBaseAddress()); + var requestBody = JsonSerializer.Serialize(new { id = 123, active = true }); using var request = new HttpRequestMessage(HttpMethod.Post, "users"); request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); @@ -327,7 +327,7 @@ public async Task GetVerifyAsync_GetBodyAsJson_ParsesCorrectly() // When await httpClient.SendAsync(request, TestContext.Current.CancellationToken); var verification = await _mockacoContainer.GetVerifyAsync("/users"); - + // Then Assert.NotNull(verification); using var jsonDoc = verification.GetBodyAsJson();