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 bf029294b..4ca50d73c 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.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
new file mode 100644
index 000000000..c948fe44e
--- /dev/null
+++ b/docs/modules/mockaco.md
@@ -0,0 +1,116 @@
+# 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.
+
+## 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"
+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") // Local folder with JSON 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 Uri(_mockacoContainer.GetBaseAddress());
+
+ // 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 Uri(_mockacoContainer.GetBaseAddress());
+
+ // 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..ab3abcf55
--- /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.14";
+
+ ///
+ /// 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..e75d33a6a
--- /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 which contains 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 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
new file mode 100644
index 000000000..a9a38a50b
--- /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 base address.
+ ///
+ /// The Mockaco base address.
+ public string GetBaseAddress()
+ {
+ 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(GetBaseAddress());
+
+ 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..d8cca88e5
--- /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.14", 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..4c451689b
--- /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.GetBaseAddress());
+ 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.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
+ 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.GetBaseAddress());
+ 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.GetBaseAddress());
+ 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.GetBaseAddress());
+ 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.GetBaseAddress());
+ 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 GetBaseAddress_ReturnsValidUri()
+ {
+ // Given & When
+ var endpoint = _mockacoContainer.GetBaseAddress();
+
+ // 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.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));
+ 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.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");
+ 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.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");
+
+ // 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;