diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 8333e0e38..82b4b70d8 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -90,6 +90,7 @@ jobs:
{ name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.ServiceBus", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Sftp", runs-on: "ubuntu-22.04" },
+ { name: "Testcontainers.Toxiproxy", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" },
diff --git a/Directory.Packages.props b/Directory.Packages.props
index be2ddadfd..6badc05f7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -85,5 +85,6 @@
+
diff --git a/Testcontainers.sln b/Testcontainers.sln
index 671d21083..0676ed2a3 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
@@ -249,6 +249,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.Toxiproxy", "src\Testcontainers.Toxiproxy\Testcontainers.Toxiproxy.csproj", "{65A47BA4-4DC8-4206-9B00-CBC87FC944FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Toxiproxy.Tests", "tests\Testcontainers.Toxiproxy.Tests\Testcontainers.Toxiproxy.Tests.csproj", "{10726AAA-E93F-4B40-A05E-28308423DABE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -727,6 +731,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
+ {65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {10726AAA-E93F-4B40-A05E-28308423DABE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {10726AAA-E93F-4B40-A05E-28308423DABE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {10726AAA-E93F-4B40-A05E-28308423DABE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {10726AAA-E93F-4B40-A05E-28308423DABE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -850,5 +862,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}
+ {65A47BA4-4DC8-4206-9B00-CBC87FC944FC} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {10726AAA-E93F-4B40-A05E-28308423DABE} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
diff --git a/docs/modules/index.md b/docs/modules/index.md
index f04973794..6c715d6f8 100644
--- a/docs/modules/index.md
+++ b/docs/modules/index.md
@@ -69,6 +69,7 @@ await moduleNameContainer.StartAsync();
| Redpanda | `docker.redpanda.com/redpandadata/redpanda:v22.2.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.Redpanda) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Redpanda) |
| Sftp | `atmoz/sftp:alpine` | [NuGet](https://www.nuget.org/packages/Testcontainers.Sftp) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Sftp) |
| SQL Server | `mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04` | [NuGet](https://www.nuget.org/packages/Testcontainers.MsSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MsSql) |
+| Toxiproxy | `ghcr.io/shopify/toxiproxy` | [NuGet](https://www.nuget.org/packages/Testcontainers.Toxiproxy) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Toxiproxy) |
| Weaviate | `semitechnologies/weaviate:1.26.14` | [NuGet](https://www.nuget.org/packages/Testcontainers.Weaviate) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Weaviate) |
| WebDriver | `selenium/standalone-chrome:110.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.WebDriver) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.WebDriver) |
diff --git a/docs/modules/toxiproxy.md b/docs/modules/toxiproxy.md
new file mode 100644
index 000000000..298ea92f9
--- /dev/null
+++ b/docs/modules/toxiproxy.md
@@ -0,0 +1,57 @@
+# Toxiproxy
+
+[Toxiproxy](https://github.com/Shopify/toxiproxy) is a proxy to simulate network failure for testing. It can simulate latency, timeouts, bandwidth limits, and more between services.
+
+This module integrates [Toxiproxy.Net](https://github.com/mdevilliers/Toxiproxy.Net), a .NET client for Toxiproxy's HTTP API. While the test suite includes examples for latency and timeout toxics, the implementation supports **all toxics and features** that Toxiproxy itself supports.
+
+## Installation
+
+Add the following dependency to your project file:
+
+```shell
+dotnet add package Testcontainers.Toxiproxy
+```
+
+## Usage Example
+
+You can start a Toxiproxy container instance and configure proxies/toxics from any .NET test or application.
+
+```csharp
+var proxyPort = 12345;
+var serverPort = 12346;
+
+var container = new ToxiproxyBuilder()
+ .WithProxy("my-proxy", $"0.0.0.0:{proxyPort}", $"host.docker.internal:{serverPort}")
+ .WithPortBinding(proxyPort, false)
+ .Build();
+
+await container.StartAsync();
+
+var proxy = container.Client.FindProxy("my-proxy");
+
+proxy.Add(new LatencyToxic
+{
+ Name = "latency-toxic",
+ Stream = ToxicDirection.DownStream,
+ Attributes = { Latency = 500 }
+});
+
+// You can use the proxy (127.0.0.1:proxyPort) to connect with the injected network condition.
+```
+
+## Available Features
+
+- Add and remove proxies dynamically
+- Inject latency, timeout, bandwidth limit, and more via toxics
+- Use `Toxiproxy.Net` to interact with the running Toxiproxy server
+- Test fault tolerance of networked services in isolated environments
+
+> Note: The library leverages the official [Toxiproxy.Net](https://github.com/mdevilliers/Toxiproxy.Net) client. Though the test suite demonstrates a couple of toxic types (e.g., latency, timeout), the module supports **all Toxiproxy features**.
+
+## Running Tests
+
+To execute the tests, use the command:
+
+```shell
+dotnet test
+```
diff --git a/src/Testcontainers.Toxiproxy/.editorconfig b/src/Testcontainers.Toxiproxy/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.Toxiproxy/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj b/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj
new file mode 100644
index 000000000..3530cccd9
--- /dev/null
+++ b/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj
@@ -0,0 +1,13 @@
+
+
+ net8.0;net9.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs b/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs
new file mode 100644
index 000000000..0080bf82b
--- /dev/null
+++ b/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs
@@ -0,0 +1,112 @@
+using Toxiproxy.Net;
+
+namespace Testcontainers.Toxiproxy;
+
+///
+[PublicAPI]
+public sealed class ToxiproxyBuilder : ContainerBuilder
+{
+ public const string ToxiproxyImage = "ghcr.io/shopify/toxiproxy";
+ public const ushort ControlPort = 8474;
+
+ private readonly List _initialProxies = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private ToxiproxyBuilder(ToxiproxyConfiguration resourceConfiguration, List initialProxies)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ _initialProxies = initialProxies;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public ToxiproxyBuilder()
+ : this(new ToxiproxyConfiguration(), new List())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ private ToxiproxyBuilder(ToxiproxyConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override ToxiproxyConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ public override ToxiproxyContainer Build()
+ {
+ Validate();
+ return new ToxiproxyContainer(DockerResourceConfiguration, _initialProxies);
+ }
+
+ ///
+ /// Initialize the default Toxiproxy configuration with image, port, and wait strategy.
+ ///
+ /// A configured instance of .
+ protected override ToxiproxyBuilder Init()
+ {
+ // Define a wait strategy that waits for the Toxiproxy HTTP API to respond with 200 OK at /proxies.
+ return base.Init()
+ .WithImage(ToxiproxyImage) // Set the Toxiproxy image.
+ .WithPortBinding(ControlPort, true) // Bind the control port.
+ .WithWaitStrategy(Wait.ForUnixContainer() // Use HTTP-based wait strategy.
+ .UntilHttpRequestIsSucceeded(request => request
+ .ForPort(ControlPort)
+ .ForPath("/proxies")
+ .ForStatusCode(System.Net.HttpStatusCode.OK)));
+ }
+
+ ///
+ /// Adds an initial proxy that will be created automatically after the container starts.
+ ///
+ /// The proxy name.
+ /// The listen address (e.g., 127.0.0.1:8888).
+ /// The upstream address (e.g., backend:80).
+ /// The builder instance.
+ public ToxiproxyBuilder WithProxy(string name, string listen, string upstream)
+ {
+ _initialProxies.Add(new Proxy
+ {
+ Name = name,
+ Enabled = true,
+ Listen = listen,
+ Upstream = upstream
+ });
+
+ return this;
+ }
+
+ ///
+ protected override void Validate()
+ {
+ base.Validate();
+ _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration)).NotNull();
+ }
+
+ ///
+ protected override ToxiproxyBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new ToxiproxyConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override ToxiproxyBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new ToxiproxyConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override ToxiproxyBuilder Merge(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue)
+ {
+ var mergedConfiguration = new ToxiproxyConfiguration(oldValue, newValue);
+ return new ToxiproxyBuilder(mergedConfiguration, new List(_initialProxies));
+ }
+}
diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs b/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs
new file mode 100644
index 000000000..c4a3efdca
--- /dev/null
+++ b/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs
@@ -0,0 +1,25 @@
+namespace Testcontainers.Toxiproxy;
+
+///
+[PublicAPI]
+public sealed class ToxiproxyConfiguration : ContainerConfiguration
+{
+ public ToxiproxyConfiguration()
+ {
+ }
+
+ public ToxiproxyConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ }
+
+ public ToxiproxyConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ }
+
+ public ToxiproxyConfiguration(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs b/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs
new file mode 100644
index 000000000..a51a789ab
--- /dev/null
+++ b/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs
@@ -0,0 +1,62 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Toxiproxy.Net;
+using ToxiproxyNetClient = Toxiproxy.Net.Client;
+
+namespace Testcontainers.Toxiproxy;
+
+///
+[PublicAPI]
+public sealed class ToxiproxyContainer : DockerContainer
+{
+ private readonly ToxiproxyConfiguration _configuration;
+ private readonly IEnumerable _initialProxies;
+ private ToxiproxyNetClient? _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ /// Optional proxies to be created automatically after startup.
+ public ToxiproxyContainer(ToxiproxyConfiguration configuration, IEnumerable? initialProxies = null)
+ : base(configuration)
+ {
+ _configuration = configuration;
+ _initialProxies = initialProxies ?? Enumerable.Empty();
+ }
+
+ ///
+ /// Gets the Toxiproxy client. Must call before accessing.
+ ///
+ public ToxiproxyNetClient Client =>
+ _client ?? throw new InvalidOperationException("Toxiproxy client is not initialized. Call StartAsync() first.");
+
+ ///
+ /// Gets the full URI of the Toxiproxy control endpoint.
+ ///
+ public Uri GetControlUri()
+ {
+ return new Uri($"http://{Hostname}:{GetMappedPublicPort(ToxiproxyBuilder.ControlPort)}");
+ }
+
+ ///
+ public override async Task StartAsync(CancellationToken ct = default)
+ {
+ await base.StartAsync(ct);
+
+ try
+ {
+ var connection = new Connection(Hostname, GetMappedPublicPort(ToxiproxyBuilder.ControlPort));
+ _client = connection.Client();
+
+ foreach (var proxy in _initialProxies)
+ {
+ _client.Add(proxy);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException("Failed to initialize Toxiproxy client or create initial proxies.", ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Toxiproxy/Usings.cs b/src/Testcontainers.Toxiproxy/Usings.cs
new file mode 100644
index 000000000..f889bad0a
--- /dev/null
+++ b/src/Testcontainers.Toxiproxy/Usings.cs
@@ -0,0 +1,10 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Linq;
+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;
+global using Microsoft.Extensions.Logging;
\ No newline at end of file
diff --git a/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj b/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj
new file mode 100644
index 000000000..0e7c12f21
--- /dev/null
+++ b/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs b/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs
new file mode 100644
index 000000000..13893a7ea
--- /dev/null
+++ b/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs
@@ -0,0 +1,446 @@
+namespace Testcontainers.Toxiproxy;
+
+///
+/// Integration tests for the Toxiproxy container module.
+///
+public sealed class ToxiproxyContainerTest : IAsyncLifetime
+{
+ private readonly ToxiproxyContainer _toxiproxyContainer = new ToxiproxyBuilder().Build();
+
+ ///
+ public Task InitializeAsync()
+ {
+ return _toxiproxyContainer.StartAsync();
+ }
+
+ ///
+ public Task DisposeAsync()
+ {
+ return _toxiproxyContainer.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ public void CanCreateAndFindProxy()
+ {
+ // Arrange
+ var client = _toxiproxyContainer.Client;
+
+ var proxy = new Proxy
+ {
+ Name = "localToGoogle",
+ Enabled = true,
+ Listen = "127.0.0.1:44399",
+ Upstream = "google.com:443"
+ };
+
+ // Act
+ client.Add(proxy);
+
+ // Assert
+ var retrievedProxy = client.FindProxy(proxy.Name);
+ Assert.NotNull(retrievedProxy);
+ Assert.Equal("localToGoogle", retrievedProxy.Name);
+ Assert.Equal("127.0.0.1:44399", retrievedProxy.Listen);
+ Assert.Equal("google.com:443", retrievedProxy.Upstream);
+ }
+
+ [Fact]
+ public void CanFindAllProxies()
+ {
+ // Arrange
+ var client = _toxiproxyContainer.Client;
+
+ var proxyOne = new Proxy
+ {
+ Name = "proxyOne",
+ Enabled = true,
+ Listen = "127.0.0.1:44400",
+ Upstream = "example.com:80"
+ };
+
+ var proxyTwo = new Proxy
+ {
+ Name = "proxyTwo",
+ Enabled = true,
+ Listen = "127.0.0.1:44401",
+ Upstream = "test.com:80"
+ };
+
+ client.Add(proxyOne);
+ client.Add(proxyTwo);
+
+ // Act
+ var allProxies = client.All();
+
+ // Assert
+ Assert.Equal(2, allProxies.Keys.Count);
+ Assert.True(allProxies.ContainsKey("proxyOne"));
+ Assert.True(allProxies.ContainsKey("proxyTwo"));
+ }
+
+ [Fact]
+ public void CanDeleteProxy()
+ {
+ // Arrange
+ var client = _toxiproxyContainer.Client;
+
+ var proxy = new Proxy
+ {
+ Name = "proxyToDelete",
+ Enabled = true,
+ Listen = "127.0.0.1:44402",
+ Upstream = "delete.com:80"
+ };
+
+ var addedProxy = client.Add(proxy);
+
+ // Act
+ addedProxy.Delete();
+
+ // Assert
+ Assert.Throws(() => client.FindProxy("proxyToDelete"));
+ }
+
+ [Fact]
+ public void CanAddSlowCloseToxic()
+ {
+ // Arrange
+ var client = _toxiproxyContainer.Client;
+
+ var proxy = new Proxy
+ {
+ Name = "proxyWithToxic",
+ Enabled = true,
+ Listen = "127.0.0.1:44403",
+ Upstream = "toxic.com:80"
+ };
+
+ var addedProxy = client.Add(proxy);
+
+ // Add a SlowCloseToxic to the proxy
+ var slowCloseToxic = new SlowCloseToxic
+ {
+ Name = "slowCloseToxic",
+ Stream = ToxicDirection.DownStream,
+ Toxicity = 0.8
+ };
+ slowCloseToxic.Attributes.Delay = 50;
+
+ addedProxy.Add(slowCloseToxic);
+ addedProxy.Update();
+
+ // Act
+ var toxics = addedProxy.GetAllToxics().ToList();
+
+ // Assert
+ Assert.Single(toxics);
+ var retrievedToxic = toxics.First() as SlowCloseToxic;
+ Assert.NotNull(retrievedToxic);
+ Assert.Equal("slowCloseToxic", retrievedToxic.Name);
+ Assert.Equal(50, retrievedToxic.Attributes.Delay);
+ Assert.Equal(ToxicDirection.DownStream, retrievedToxic.Stream);
+ }
+
+ [Fact]
+ public void CreatingDuplicateProxyThrows()
+ {
+ // Arrange
+ var client = _toxiproxyContainer.Client;
+ var proxy = new Proxy
+ {
+ Name = "duplicate",
+ Enabled = true,
+ Listen = "127.0.0.1:44500",
+ Upstream = "service:80"
+ };
+ client.Add(proxy);
+
+ // Act & Assert
+ Assert.Throws(() => client.Add(proxy));
+ }
+
+ [Fact]
+ public void InvalidListenAddressThrows()
+ {
+ var client = _toxiproxyContainer.Client;
+
+ var proxy = new Proxy
+ {
+ Name = "invalidListen",
+ Enabled = true,
+ Listen = "notaport",
+ Upstream = "localhost:1234"
+ };
+
+ Assert.Throws(() => client.Add(proxy));
+ }
+
+ [Fact]
+ public void CanDisableProxy()
+ {
+ var client = _toxiproxyContainer.Client;
+
+ var proxy = new Proxy
+ {
+ Name = "disabledProxy",
+ Enabled = true,
+ Listen = "127.0.0.1:44501",
+ Upstream = "service.com:80"
+ };
+
+ var added = client.Add(proxy);
+ added.Enabled = false;
+ added.Update();
+
+ var updated = client.FindProxy("disabledProxy");
+ Assert.False(updated.Enabled);
+ }
+
+ [Fact]
+ public void CanRemoveAllToxics()
+ {
+ var client = _toxiproxyContainer.Client;
+
+ var proxy = new Proxy
+ {
+ Name = "proxyWithToxics",
+ Enabled = true,
+ Listen = "127.0.0.1:44503",
+ Upstream = "api:80"
+ };
+
+ var added = client.Add(proxy);
+
+ var toxic = new SlowCloseToxic
+ {
+ Name = "slow",
+ Stream = ToxicDirection.DownStream,
+ Toxicity = 1.0,
+ Attributes = { Delay = 100 }
+ };
+
+ added.Add(toxic);
+ added.RemoveToxic("slow");
+
+ var toxics = added.GetAllToxics();
+ Assert.Empty(toxics);
+ }
+
+ [Fact]
+ public async Task LatencyToxicConfigurationIsApplied()
+ {
+ var container = new ToxiproxyBuilder()
+ .WithProxy("latency", "0.0.0.0:12345", "localhost:12346")
+ .WithPortBinding(12345, true)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilHttpRequestIsSucceeded(req => req
+ .ForPort(8474)
+ .ForPath("/proxies")))
+ .Build();
+
+ await container.StartAsync();
+ await WaitForProxyToBeReady(container.Client, "latency", TimeSpan.FromSeconds(10));
+
+ var proxy = container.Client.FindProxy("latency");
+
+ proxy.Add(new LatencyToxic
+ {
+ Name = "latency-toxic",
+ Stream = ToxicDirection.DownStream,
+ Attributes = { Latency = 500 }
+ });
+
+ await Task.Delay(250);
+
+ var updated = container.Client.FindProxy("latency");
+ var toxics = updated.GetAllToxics();
+ var toxic = toxics.FirstOrDefault(t => t.Name == "latency-toxic") as LatencyToxic;
+
+ Assert.NotNull(toxic);
+ Assert.Equal(500, toxic.Attributes.Latency);
+ }
+
+ [Fact]
+ public async Task LatencyToxic_ShouldIntroduceExpectedDelay()
+ {
+ var serverPort = GetFreePort();
+ var listener = new TcpListener(IPAddress.Loopback, serverPort);
+ listener.Start();
+
+ _ = Task.Run(async () =>
+ {
+ using var serverClient = await listener.AcceptTcpClientAsync();
+ using var stream = serverClient.GetStream();
+ var buffer = new byte[1024];
+ int bytesRead = await stream.ReadAsync(buffer);
+ await stream.WriteAsync(buffer, 0, bytesRead);
+ });
+
+ var proxyPort = GetFreePort();
+ var proxyName = "latency-proxy";
+ var container = new ToxiproxyBuilder()
+ .WithProxy(proxyName, $"0.0.0.0:{proxyPort}", $"host.docker.internal:{serverPort}")
+ .WithPortBinding(proxyPort, false)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilHttpRequestIsSucceeded(req => req
+ .ForPort(8474)
+ .ForPath("/proxies")
+ .ForStatusCode(HttpStatusCode.OK)))
+ .Build();
+
+ await container.StartAsync();
+
+ var proxy = container.Client.FindProxy(proxyName);
+ proxy.Add(new LatencyToxic
+ {
+ Name = "latency",
+ Stream = ToxicDirection.DownStream,
+ Toxicity = 1.0,
+ Attributes = { Latency = 500 }
+ });
+
+ using var client = new TcpClient();
+ await client.ConnectAsync("127.0.0.1", proxyPort);
+ using var stream = client.GetStream();
+
+ var message = Encoding.UTF8.GetBytes("test");
+ var buffer = new byte[message.Length];
+
+ var sw = Stopwatch.StartNew();
+ await stream.WriteAsync(message);
+ await stream.ReadAsync(buffer);
+ sw.Stop();
+
+ var delay = sw.ElapsedMilliseconds;
+
+ await container.DisposeAsync();
+ listener.Stop();
+
+ const int expectedMin = 450;
+ const int expectedMax = 2000;
+ Assert.True(delay >= expectedMin && delay <= expectedMax,
+ $"Expected delay between {expectedMin}ms and {expectedMax}ms, but got {delay}ms");
+ }
+
+
+ private static async Task WaitForProxyToBeActive(Client client, string proxyName, string host, int port, TimeSpan timeout)
+ {
+ var start = DateTime.UtcNow;
+ Exception? lastException = null;
+
+ while (DateTime.UtcNow - start < timeout)
+ {
+ try
+ {
+ var proxy = client.FindProxy(proxyName);
+ if (proxy.Enabled)
+ {
+ using var tcp = new TcpClient();
+ await tcp.ConnectAsync(host, port);
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+ await Task.Delay(100);
+ }
+ }
+
+ throw new TimeoutException($"Proxy '{proxyName}' on {host}:{port} did not become ready in time.", lastException);
+ }
+
+ [Fact]
+ public async Task TimeoutToxic_ShouldDropConnectionAfterTimeout()
+ {
+ var serverPort = GetFreePort();
+ var listener = new TcpListener(IPAddress.Loopback, serverPort);
+ listener.Start();
+
+ _ = Task.Run(async () =>
+ {
+ using var serverClient = await listener.AcceptTcpClientAsync();
+ using var stream = serverClient.GetStream();
+ var buffer = new byte[1024];
+ await stream.ReadAsync(buffer);
+ });
+
+ var proxyPort = GetFreePort();
+ var proxyName = "timeout-proxy";
+
+ var container = new ToxiproxyBuilder()
+ .WithProxy(proxyName, $"0.0.0.0:{proxyPort}", $"host.docker.internal:{serverPort}")
+ .WithPortBinding(proxyPort, false)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilHttpRequestIsSucceeded(req => req
+ .ForPort(8474)
+ .ForPath("/proxies")
+ .ForStatusCode(HttpStatusCode.OK)))
+ .Build();
+
+ await container.StartAsync();
+ var proxy = container.Client.FindProxy(proxyName);
+
+ proxy.Add(new TimeoutToxic
+ {
+ Name = "timeout-toxic",
+ Stream = ToxicDirection.UpStream,
+ Toxicity = 1.0,
+ Attributes = { Timeout = 1000 }
+ });
+
+ using var client = new TcpClient();
+ await client.ConnectAsync("127.0.0.1", proxyPort);
+
+ using var stream = client.GetStream();
+ var payload = Encoding.UTF8.GetBytes("test");
+
+ var sw = Stopwatch.StartNew();
+ Exception? ex = await Record.ExceptionAsync(async () =>
+ {
+ await stream.WriteAsync(payload);
+ await stream.ReadAsync(new byte[5]);
+ });
+ sw.Stop();
+
+ await container.DisposeAsync();
+ listener.Stop();
+ Assert.True(sw.ElapsedMilliseconds >= 1000, $"Expected timeout after >= 1000ms but took {sw.ElapsedMilliseconds}ms.");
+ }
+
+
+ private static async Task WaitForProxyToBeReady(Client client, string proxyName, TimeSpan timeout)
+ {
+ var start = DateTime.UtcNow;
+ Exception? lastError = null;
+
+ while (DateTime.UtcNow - start < timeout)
+ {
+ try
+ {
+ var proxy = client.FindProxy(proxyName);
+ if (!string.IsNullOrEmpty(proxy.Listen))
+ {
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ lastError = ex;
+ }
+
+ await Task.Delay(100);
+ }
+
+ throw new TimeoutException($"Proxy '{proxyName}' did not become ready in time.", lastError);
+ }
+
+ private static int GetFreePort()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+}
diff --git a/tests/Testcontainers.Toxiproxy.Tests/Usings.cs b/tests/Testcontainers.Toxiproxy.Tests/Usings.cs
new file mode 100644
index 000000000..8a2461034
--- /dev/null
+++ b/tests/Testcontainers.Toxiproxy.Tests/Usings.cs
@@ -0,0 +1,9 @@
+// Global using directives
+
+global using System.Diagnostics;
+global using System.Net;
+global using System.Net.Sockets;
+global using System.Text;
+global using DotNet.Testcontainers.Builders;
+global using Toxiproxy.Net;
+global using Toxiproxy.Net.Toxics;
\ No newline at end of file