From b9d94c9492e76cdbec29192c9f1df35e9bd65f9a Mon Sep 17 00:00:00 2001 From: iltertaha Date: Thu, 17 Oct 2024 23:24:18 +0100 Subject: [PATCH 1/8] Create toxiproxy module template --- Testcontainers.sln | 9 +- src/Testcontainers.Toxiproxy/.editorconfig | 1 + .../Testcontainers.Toxiproxy.csproj | 12 +++ .../ToxiproxyBuilder.cs | 86 +++++++++++++++++++ .../ToxiproxyConfiguration.cs | 63 ++++++++++++++ .../ToxiproxyContainer.cs | 15 ++++ src/Testcontainers.Toxiproxy/Usings.cs | 10 +++ 7 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/Testcontainers.Toxiproxy/.editorconfig create mode 100644 src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj create mode 100644 src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs create mode 100644 src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs create mode 100644 src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs create mode 100644 src/Testcontainers.Toxiproxy/Usings.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index 10cae3fc2..e3d501489 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 @@ -195,6 +195,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Toxiproxy", "src\Testcontainers.Toxiproxy\Testcontainers.Toxiproxy.csproj", "{52091402-4A94-43BF-B57A-3CF8E00B29D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -568,6 +570,10 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {52091402-4A94-43BF-B57A-3CF8E00B29D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52091402-4A94-43BF-B57A-3CF8E00B29D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52091402-4A94-43BF-B57A-3CF8E00B29D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52091402-4A94-43BF-B57A-3CF8E00B29D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -661,5 +667,6 @@ Global {9E8E6AA5-65D1-498F-BEAB-BA34723A0050} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {52091402-4A94-43BF-B57A-3CF8E00B29D2} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} EndGlobalSection EndGlobal 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..8b2ed72c6 --- /dev/null +++ b/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj @@ -0,0 +1,12 @@ + + + net6.0;net8.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..83db2fb32 --- /dev/null +++ b/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs @@ -0,0 +1,86 @@ +namespace Testcontainers.Toxiproxy; + +/// +[PublicAPI] +public sealed class ToxiproxyBuilder : ContainerBuilder +{ + /// + /// Initializes a new instance of the class. + /// + public ToxiproxyBuilder() + : this(new ToxiproxyConfiguration()) + { + // 1) To change the ContainerBuilder default configuration override the DockerResourceConfiguration property and the "ToxiproxyBuilder Init()" method. + // Append the module configuration to base.Init() e.g. base.Init().WithImage("alpine:3.17") to set the modules' default image. + + // 2) To customize the ContainerBuilder validation override the "void Validate()" method. + // Use Testcontainers' Guard.Argument(TType, string) or your own guard implementation to validate the module configuration. + + // 3) Add custom builder methods to extend the ContainerBuilder capabilities such as "ToxiproxyBuilder WithToxiproxyConfig(object)". + // Merge the current module configuration with a new instance of the immutable ToxiproxyConfiguration type to update the module configuration. + + // DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private ToxiproxyBuilder(ToxiproxyConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // DockerResourceConfiguration = resourceConfiguration; + } + + // /// + // protected override ToxiproxyConfiguration DockerResourceConfiguration { get; } + + // /// + // /// Sets the Toxiproxy config. + // /// + // /// The Toxiproxy config. + // /// A configured instance of . + // public ToxiproxyBuilder WithToxiproxyConfig(object config) + // { + // // Extends the ContainerBuilder capabilities and holds a custom configuration in ToxiproxyConfiguration. + // // In case of a module requires other properties to represent itself, extend ContainerConfiguration. + // return Merge(DockerResourceConfiguration, new ToxiproxyConfiguration(config: config)); + // } + + /// + public override ToxiproxyContainer Build() + { + Validate(); + return new ToxiproxyContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + // /// + // protected override ToxiproxyBuilder Init() + // { + // return base.Init(); + // } + + // /// + // protected override void Validate() + // { + // base.Validate(); + // } + + /// + 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) + { + return new ToxiproxyBuilder(new ToxiproxyConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs b/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs new file mode 100644 index 000000000..85ebe7e93 --- /dev/null +++ b/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs @@ -0,0 +1,63 @@ +namespace Testcontainers.Toxiproxy; + +/// +[PublicAPI] +public sealed class ToxiproxyConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The Toxiproxy config. + public ToxiproxyConfiguration(object config = null) + { + // // Sets the custom builder methods property values. + // Config = config; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public ToxiproxyConfiguration(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 ToxiproxyConfiguration(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 ToxiproxyConfiguration(ToxiproxyConfiguration resourceConfiguration) + : this(new ToxiproxyConfiguration(), 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 ToxiproxyConfiguration(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue) + : base(oldValue, newValue) + { + // // Create an updated immutable copy of the module configuration. + // Config = BuildConfiguration.Combine(oldValue.Config, newValue.Config); + } + + // /// + // /// Gets the Toxiproxy config. + // /// + // public object Config { get; } +} \ 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..d8a72d77a --- /dev/null +++ b/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs @@ -0,0 +1,15 @@ +namespace Testcontainers.Toxiproxy; + +/// +[PublicAPI] +public sealed class ToxiproxyContainer : DockerContainer +{ + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public ToxiproxyContainer(ToxiproxyConfiguration configuration) + : base(configuration) + { + } +} \ 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 From 5879ba5da4a8edb071d484edaa88bf7aca14953e Mon Sep 17 00:00:00 2001 From: iltertaha Date: Thu, 17 Oct 2024 23:29:30 +0100 Subject: [PATCH 2/8] Add initial implementation --- Directory.Packages.props | 129 +++++++++--------- .../Testcontainers.Toxiproxy.csproj | 5 +- .../ToxiproxyBuilder.cs | 73 +++++----- .../ToxiproxyConfiguration.cs | 40 +----- .../ToxiproxyContainer.cs | 43 +++++- 5 files changed, 143 insertions(+), 147 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fbc55db0a..99ecba286 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,67 +1,68 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj b/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj index 8b2ed72c6..0360b98d1 100644 --- a/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj +++ b/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj @@ -4,9 +4,10 @@ latest - + + - + \ No newline at end of file diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs b/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs index 83db2fb32..5146829e6 100644 --- a/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs +++ b/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs @@ -1,25 +1,20 @@ + namespace Testcontainers.Toxiproxy; /// [PublicAPI] public sealed class ToxiproxyBuilder : ContainerBuilder { + public const string ToxiproxyImage = "ghcr.io/shopify/toxiproxy"; + public const ushort ControlPort = 8474; + /// /// Initializes a new instance of the class. /// public ToxiproxyBuilder() : this(new ToxiproxyConfiguration()) { - // 1) To change the ContainerBuilder default configuration override the DockerResourceConfiguration property and the "ToxiproxyBuilder Init()" method. - // Append the module configuration to base.Init() e.g. base.Init().WithImage("alpine:3.17") to set the modules' default image. - - // 2) To customize the ContainerBuilder validation override the "void Validate()" method. - // Use Testcontainers' Guard.Argument(TType, string) or your own guard implementation to validate the module configuration. - - // 3) Add custom builder methods to extend the ContainerBuilder capabilities such as "ToxiproxyBuilder WithToxiproxyConfig(object)". - // Merge the current module configuration with a new instance of the immutable ToxiproxyConfiguration type to update the module configuration. - - // DockerResourceConfiguration = Init().DockerResourceConfiguration; + DockerResourceConfiguration = Init().DockerResourceConfiguration; } /// @@ -29,42 +24,42 @@ public ToxiproxyBuilder() private ToxiproxyBuilder(ToxiproxyConfiguration resourceConfiguration) : base(resourceConfiguration) { - // DockerResourceConfiguration = resourceConfiguration; + DockerResourceConfiguration = resourceConfiguration; } - // /// - // protected override ToxiproxyConfiguration DockerResourceConfiguration { get; } - - // /// - // /// Sets the Toxiproxy config. - // /// - // /// The Toxiproxy config. - // /// A configured instance of . - // public ToxiproxyBuilder WithToxiproxyConfig(object config) - // { - // // Extends the ContainerBuilder capabilities and holds a custom configuration in ToxiproxyConfiguration. - // // In case of a module requires other properties to represent itself, extend ContainerConfiguration. - // return Merge(DockerResourceConfiguration, new ToxiproxyConfiguration(config: config)); - // } + /// + protected override ToxiproxyConfiguration DockerResourceConfiguration { get; } /// public override ToxiproxyContainer Build() { Validate(); - return new ToxiproxyContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + return new ToxiproxyContainer(DockerResourceConfiguration); } - // /// - // protected override ToxiproxyBuilder Init() - // { - // return base.Init(); - // } + /// + /// 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 CLI command `list` to complete successfully. + + + return base.Init() + .WithImage(ToxiproxyImage) // Set the Toxiproxy image. + .WithPortBinding(ControlPort, true) // Bind the control port. + ; // Use the defined wait strategy. + } - // /// - // protected override void Validate() - // { - // base.Validate(); - // } + /// + protected override void Validate() + { + base.Validate(); + + // Validate that the DockerResourceConfiguration is properly set. + _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration)).NotNull(); + } /// protected override ToxiproxyBuilder Clone(IResourceConfiguration resourceConfiguration) @@ -81,6 +76,8 @@ protected override ToxiproxyBuilder Clone(IContainerConfiguration resourceConfig /// protected override ToxiproxyBuilder Merge(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue) { - return new ToxiproxyBuilder(new ToxiproxyConfiguration(oldValue, newValue)); + // Merge the old and new configurations into an immutable copy. + var mergedConfiguration = new ToxiproxyConfiguration(oldValue, newValue); + return new ToxiproxyBuilder(mergedConfiguration); } -} \ No newline at end of file +} diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs b/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs index 85ebe7e93..c4a3efdca 100644 --- a/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs +++ b/src/Testcontainers.Toxiproxy/ToxiproxyConfiguration.cs @@ -4,60 +4,22 @@ namespace Testcontainers.Toxiproxy; [PublicAPI] public sealed class ToxiproxyConfiguration : ContainerConfiguration { - /// - /// Initializes a new instance of the class. - /// - /// The Toxiproxy config. - public ToxiproxyConfiguration(object config = null) + public ToxiproxyConfiguration() { - // // Sets the custom builder methods property values. - // Config = config; } - /// - /// Initializes a new instance of the class. - /// - /// The Docker resource configuration. public ToxiproxyConfiguration(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 ToxiproxyConfiguration(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 ToxiproxyConfiguration(ToxiproxyConfiguration resourceConfiguration) - : this(new ToxiproxyConfiguration(), 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 ToxiproxyConfiguration(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue) : base(oldValue, newValue) { - // // Create an updated immutable copy of the module configuration. - // Config = BuildConfiguration.Combine(oldValue.Config, newValue.Config); } - - // /// - // /// Gets the Toxiproxy config. - // /// - // public object Config { get; } } \ No newline at end of file diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs b/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs index d8a72d77a..7cac537ee 100644 --- a/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs +++ b/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs @@ -1,15 +1,50 @@ +using System.Threading; +using System.Threading.Tasks; +using Toxiproxy.Net; +using ToxiproxyNetClient = Toxiproxy.Net.Client; + namespace Testcontainers.Toxiproxy; /// [PublicAPI] public sealed class ToxiproxyContainer : DockerContainer { - /// - /// Initializes a new instance of the class. - /// - /// The container configuration. + private readonly ToxiproxyConfiguration _configuration; + + private ToxiproxyNetClient _client; + public ToxiproxyContainer(ToxiproxyConfiguration configuration) : base(configuration) { + _configuration = configuration; + } + + /// + /// Gets the Toxiproxy connection string. + /// + /// The Toxiproxy control endpoint URL. + public string GetConnectionString() + { + return $"http://{Hostname}:{GetMappedPublicPort(ToxiproxyBuilder.ControlPort)}"; + } + + /// + /// Starts the container and initializes the Toxiproxy client. + /// + public override async Task StartAsync(CancellationToken ct = default) + { + await base.StartAsync(ct); + var connection = new Connection(Hostname, GetMappedPublicPort(ToxiproxyBuilder.ControlPort)); + _client = connection.Client(); + } + + public string GetHost() + { + return Hostname; + } + + public int GetControlPort() + { + return GetMappedPublicPort(ToxiproxyBuilder.ControlPort); } } \ No newline at end of file From a5335642cb20914ff5a5f33efaf12a16722f474f Mon Sep 17 00:00:00 2001 From: iltertaha Date: Thu, 17 Oct 2024 23:32:32 +0100 Subject: [PATCH 3/8] Remove signature check for local builds --- Directory.Build.props | 1 - 1 file changed, 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index f0c488fef..de12701bd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,7 +21,6 @@ https://github.com/testcontainers/testcontainers-dotnet - $(MSBuildThisFileDirectory)src/strongname.snk true embedded From 9a7ba96ba841817c86d36943f7004a818f768cee Mon Sep 17 00:00:00 2001 From: iltertaha Date: Thu, 17 Oct 2024 23:36:37 +0100 Subject: [PATCH 4/8] Add initial tests --- Testcontainers.sln | 7 + .../Testcontainers.Toxiproxy.Tests.csproj | 27 +++ .../ToxiproxyContainerTest.cs | 154 ++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj create mode 100644 tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index e3d501489..9916ed474 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -197,6 +197,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Toxiproxy", "src\Testcontainers.Toxiproxy\Testcontainers.Toxiproxy.csproj", "{52091402-4A94-43BF-B57A-3CF8E00B29D2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Toxiproxy.Tests", "tests\Testcontainers.Toxiproxy.Tests\Testcontainers.Toxiproxy.Tests.csproj", "{2B3F08C6-9F14-4ED0-A5AF-5E70FABB7DFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -574,6 +576,10 @@ Global {52091402-4A94-43BF-B57A-3CF8E00B29D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {52091402-4A94-43BF-B57A-3CF8E00B29D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {52091402-4A94-43BF-B57A-3CF8E00B29D2}.Release|Any CPU.Build.0 = Release|Any CPU + {2B3F08C6-9F14-4ED0-A5AF-5E70FABB7DFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B3F08C6-9F14-4ED0-A5AF-5E70FABB7DFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B3F08C6-9F14-4ED0-A5AF-5E70FABB7DFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B3F08C6-9F14-4ED0-A5AF-5E70FABB7DFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -668,5 +674,6 @@ Global {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {52091402-4A94-43BF-B57A-3CF8E00B29D2} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {2B3F08C6-9F14-4ED0-A5AF-5E70FABB7DFB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal 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..2e38f7287 --- /dev/null +++ b/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.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..96bd4a9e1 --- /dev/null +++ b/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs @@ -0,0 +1,154 @@ +using Toxiproxy.Net; +using Toxiproxy.Net.Toxics; + +namespace Testcontainers.Toxiproxy +{ + public sealed class ToxiproxyContainerTest : IAsyncLifetime + { + private readonly ToxiproxyContainer _toxiproxyContainer = new ToxiproxyBuilder().Build(); + + public Task InitializeAsync() + { + // Start the Toxiproxy container + return _toxiproxyContainer.StartAsync(); + } + + public Task DisposeAsync() + { + // Dispose the container when the test finishes + return _toxiproxyContainer.DisposeAsync().AsTask(); + } + + [Fact] + public void CanCreateAndFindProxy() + { + // Arrange + var connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); + var client = connection.Client(); + + // Create a proxy for traffic from 127.0.0.1:44399 to google.com:443 + 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 connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); + var client = connection.Client(); + + // Create two proxies + 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 connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); + var client = connection.Client(); + + // Create and add a proxy + var proxy = new Proxy + { + Name = "proxyToDelete", + Enabled = true, + Listen = "127.0.0.1:44402", + Upstream = "delete.com:80" + }; + + var addedProxy = client.Add(proxy); + + // Act - Delete the proxy + addedProxy.Delete(); + + // Assert - Verify that the proxy no longer exists + Assert.Throws(() => client.FindProxy("proxyToDelete")); + } + + [Fact] + public void CanAddSlowCloseToxic() + { + // Arrange + var connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); + var client = connection.Client(); + + // Create and add a proxy + 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 // Setting the toxicity level + }; + slowCloseToxic.Attributes.Delay = 50; // Delay in milliseconds + + // Add the toxic to the proxy and update + addedProxy.Add(slowCloseToxic); + addedProxy.Update(); + + // Act - Retrieve all toxics from the proxy + var toxics = addedProxy.GetAllToxics().ToList(); + + // Assert - Check if the toxic was correctly added + 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); + } + + } +} From 6312313a7a61a680d2e2fed1659c8af53d3e3a42 Mon Sep 17 00:00:00 2001 From: iltertaha Date: Sun, 25 May 2025 17:09:49 +0100 Subject: [PATCH 5/8] Add Toxiproxy support and refactor tests --- .../ToxiproxyBuilder.cs | 55 +- .../ToxiproxyContainer.cs | 54 +- .../ToxiproxyContainerTest.cs | 534 ++++++++++++++---- .../Testcontainers.Toxiproxy.Tests/Usings.cs | 9 + 4 files changed, 497 insertions(+), 155 deletions(-) create mode 100644 tests/Testcontainers.Toxiproxy.Tests/Usings.cs diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs b/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs index 5146829e6..0080bf82b 100644 --- a/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs +++ b/src/Testcontainers.Toxiproxy/ToxiproxyBuilder.cs @@ -1,3 +1,4 @@ +using Toxiproxy.Net; namespace Testcontainers.Toxiproxy; @@ -8,19 +9,28 @@ public sealed class ToxiproxyBuilder : ContainerBuilder _initialProxies = new(); + /// /// Initializes a new instance of the class. /// - public ToxiproxyBuilder() - : this(new ToxiproxyConfiguration()) + private ToxiproxyBuilder(ToxiproxyConfiguration resourceConfiguration, List initialProxies) + : base(resourceConfiguration) { - DockerResourceConfiguration = Init().DockerResourceConfiguration; + 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) { @@ -34,7 +44,7 @@ private ToxiproxyBuilder(ToxiproxyConfiguration resourceConfiguration) public override ToxiproxyContainer Build() { Validate(); - return new ToxiproxyContainer(DockerResourceConfiguration); + return new ToxiproxyContainer(DockerResourceConfiguration, _initialProxies); } /// @@ -43,21 +53,41 @@ public override ToxiproxyContainer Build() /// A configured instance of . protected override ToxiproxyBuilder Init() { - // Define a wait strategy that waits for the Toxiproxy CLI command `list` to complete successfully. + // 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 base.Init() - .WithImage(ToxiproxyImage) // Set the Toxiproxy image. - .WithPortBinding(ControlPort, true) // Bind the control port. - ; // Use the defined wait strategy. + return this; } /// protected override void Validate() { base.Validate(); - - // Validate that the DockerResourceConfiguration is properly set. _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration)).NotNull(); } @@ -76,8 +106,7 @@ protected override ToxiproxyBuilder Clone(IContainerConfiguration resourceConfig /// protected override ToxiproxyBuilder Merge(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue) { - // Merge the old and new configurations into an immutable copy. var mergedConfiguration = new ToxiproxyConfiguration(oldValue, newValue); - return new ToxiproxyBuilder(mergedConfiguration); + return new ToxiproxyBuilder(mergedConfiguration, new List(_initialProxies)); } } diff --git a/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs b/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs index 7cac537ee..a51a789ab 100644 --- a/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs +++ b/src/Testcontainers.Toxiproxy/ToxiproxyContainer.cs @@ -10,41 +10,53 @@ namespace Testcontainers.Toxiproxy; public sealed class ToxiproxyContainer : DockerContainer { private readonly ToxiproxyConfiguration _configuration; + private readonly IEnumerable _initialProxies; + private ToxiproxyNetClient? _client; - private ToxiproxyNetClient _client; - - public ToxiproxyContainer(ToxiproxyConfiguration configuration) + /// + /// 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 connection string. + /// Gets the Toxiproxy client. Must call before accessing. /// - /// The Toxiproxy control endpoint URL. - public string GetConnectionString() - { - return $"http://{Hostname}:{GetMappedPublicPort(ToxiproxyBuilder.ControlPort)}"; - } + public ToxiproxyNetClient Client => + _client ?? throw new InvalidOperationException("Toxiproxy client is not initialized. Call StartAsync() first."); /// - /// Starts the container and initializes the Toxiproxy client. + /// Gets the full URI of the Toxiproxy control endpoint. /// - public override async Task StartAsync(CancellationToken ct = default) + public Uri GetControlUri() { - await base.StartAsync(ct); - var connection = new Connection(Hostname, GetMappedPublicPort(ToxiproxyBuilder.ControlPort)); - _client = connection.Client(); - } - - public string GetHost() - { - return Hostname; + return new Uri($"http://{Hostname}:{GetMappedPublicPort(ToxiproxyBuilder.ControlPort)}"); } - public int GetControlPort() + /// + public override async Task StartAsync(CancellationToken ct = default) { - return GetMappedPublicPort(ToxiproxyBuilder.ControlPort); + 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/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs b/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs index 96bd4a9e1..13893a7ea 100644 --- a/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs +++ b/tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs @@ -1,154 +1,446 @@ -using Toxiproxy.Net; -using Toxiproxy.Net.Toxics; +namespace Testcontainers.Toxiproxy; -namespace Testcontainers.Toxiproxy +/// +/// Integration tests for the Toxiproxy container module. +/// +public sealed class ToxiproxyContainerTest : IAsyncLifetime { - public sealed class ToxiproxyContainerTest : IAsyncLifetime + private readonly ToxiproxyContainer _toxiproxyContainer = new ToxiproxyBuilder().Build(); + + /// + public Task InitializeAsync() { - private readonly ToxiproxyContainer _toxiproxyContainer = new ToxiproxyBuilder().Build(); + return _toxiproxyContainer.StartAsync(); + } - public Task InitializeAsync() - { - // Start the Toxiproxy container - return _toxiproxyContainer.StartAsync(); - } + /// + public Task DisposeAsync() + { + return _toxiproxyContainer.DisposeAsync().AsTask(); + } - public Task DisposeAsync() - { - // Dispose the container when the test finishes - return _toxiproxyContainer.DisposeAsync().AsTask(); - } + [Fact] + public void CanCreateAndFindProxy() + { + // Arrange + var client = _toxiproxyContainer.Client; - [Fact] - public void CanCreateAndFindProxy() + var proxy = new Proxy { - // Arrange - var connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); - var client = connection.Client(); - - // Create a proxy for traffic from 127.0.0.1:44399 to google.com:443 - 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); - } + 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; - [Fact] - public void CanFindAllProxies() + var proxyOne = new Proxy { - // Arrange - var connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); - var client = connection.Client(); - - // Create two proxies - 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")); - } + Name = "proxyOne", + Enabled = true, + Listen = "127.0.0.1:44400", + Upstream = "example.com:80" + }; - [Fact] - public void CanDeleteProxy() + var proxyTwo = new Proxy { - // Arrange - var connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); - var client = connection.Client(); + Name = "proxyTwo", + Enabled = true, + Listen = "127.0.0.1:44401", + Upstream = "test.com:80" + }; - // Create and add a proxy - var proxy = new Proxy - { + 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); + var addedProxy = client.Add(proxy); - // Act - Delete the proxy - addedProxy.Delete(); + // Act + addedProxy.Delete(); - // Assert - Verify that the proxy no longer exists - Assert.Throws(() => client.FindProxy("proxyToDelete")); - } + // Assert + Assert.Throws(() => client.FindProxy("proxyToDelete")); + } - [Fact] - public void CanAddSlowCloseToxic() - { - // Arrange - var connection = new Connection(_toxiproxyContainer.GetHost(), _toxiproxyContainer.GetControlPort()); - var client = connection.Client(); + [Fact] + public void CanAddSlowCloseToxic() + { + // Arrange + var client = _toxiproxyContainer.Client; - // Create and add a proxy - var proxy = new Proxy - { + var proxy = new Proxy + { Name = "proxyWithToxic", Enabled = true, Listen = "127.0.0.1:44403", Upstream = "toxic.com:80" - }; + }; - var addedProxy = client.Add(proxy); + var addedProxy = client.Add(proxy); - // Add a SlowCloseToxic to the proxy - var slowCloseToxic = new SlowCloseToxic - { + // Add a SlowCloseToxic to the proxy + var slowCloseToxic = new SlowCloseToxic + { Name = "slowCloseToxic", Stream = ToxicDirection.DownStream, - Toxicity = 0.8 // Setting the toxicity level - }; - slowCloseToxic.Attributes.Delay = 50; // Delay in milliseconds - - // Add the toxic to the proxy and update - addedProxy.Add(slowCloseToxic); - addedProxy.Update(); - - // Act - Retrieve all toxics from the proxy - var toxics = addedProxy.GetAllToxics().ToList(); - - // Assert - Check if the toxic was correctly added - 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); + 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 From 96a9cc4e31dd19a04247958d4e117f8a4c50690f Mon Sep 17 00:00:00 2001 From: iltertaha Date: Sun, 25 May 2025 17:38:11 +0100 Subject: [PATCH 6/8] Revert omitted assembly key file --- Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Build.props b/Directory.Build.props index fbc7e662c..c35efb8c5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,6 +21,7 @@ https://github.com/testcontainers/testcontainers-dotnet + $(MSBuildThisFileDirectory)src/strongname.snk true embedded From 138b1c834f5c57052d75cfaf1fcb277b6883ef41 Mon Sep 17 00:00:00 2001 From: iltertaha Date: Sun, 25 May 2025 18:22:21 +0100 Subject: [PATCH 7/8] Update target framework for Toxiproxy and add projects to the sln --- Testcontainers.sln | 27 +++++++++++++++++-- .../Testcontainers.Toxiproxy.csproj | 2 +- .../Testcontainers.Toxiproxy.Tests.csproj | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Testcontainers.sln b/Testcontainers.sln index 30c39ee07..0676ed2a3 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -249,9 +249,9 @@ 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", "{52091402-4A94-43BF-B57A-3CF8E00B29D2}" +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", "{2B3F08C6-9F14-4ED0-A5AF-5E70FABB7DFB}" +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 @@ -723,6 +723,25 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.Build.0 = Release|Any CPU + {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -841,5 +860,9 @@ Global {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {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/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj b/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj index 0360b98d1..3530cccd9 100644 --- a/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj +++ b/src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj @@ -1,6 +1,6 @@ - net6.0;net8.0;netstandard2.0;netstandard2.1 + net8.0;net9.0;netstandard2.0;netstandard2.1 latest diff --git a/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj b/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj index 2e38f7287..0e7c12f21 100644 --- a/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj +++ b/tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable From d5e4304b082678b58a680379ce2fc3bab9c68a10 Mon Sep 17 00:00:00 2001 From: iltertaha Date: Sun, 25 May 2025 19:08:34 +0100 Subject: [PATCH 8/8] Add toxiproxy doc and links, update pipeline steps --- .github/workflows/cicd.yml | 1 + docs/modules/index.md | 1 + docs/modules/toxiproxy.md | 57 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 docs/modules/toxiproxy.md 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/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 +```