diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 87dc5d3b1..02852656b 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -81,6 +81,7 @@ jobs: { name: "Testcontainers.PostgreSql", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.PubSub", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Pulsar", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Qdrant", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.RabbitMq", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.RavenDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Redis", runs-on: "ubuntu-22.04" }, diff --git a/Directory.Build.props b/Directory.Build.props index 26aa57df5..2a7cef183 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ $(AssemblyName) - 4.3.0 + 4.4.0 $(Version) $(Version) Testcontainers diff --git a/Directory.Packages.props b/Directory.Packages.props index 1f706b849..c73d7305c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -70,6 +70,7 @@ + diff --git a/Testcontainers.sln b/Testcontainers.sln index 14af32b18..720ad9de1 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FirebirdSql" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Firestore", "src\Testcontainers.Firestore\Testcontainers.Firestore.csproj", "{B3CC460D-0DFD-48A8-9502-54E9828B7B05}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.GCloud", "src\Testcontainers.GCloud\Testcontainers.GCloud.csproj", "{D7CE8744-E58B-4277-BBB7-6840A4FF2049}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.InfluxDb", "src\Testcontainers.InfluxDb\Testcontainers.InfluxDb.csproj", "{8F483B83-7BD4-4BD5-9F03-DFC26E1CE678}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.JanusGraph", "src\Testcontainers.JanusGraph\Testcontainers.JanusGraph.csproj", "{C5AF86A8-2F11-41B6-BB01-325AD9016B94}" @@ -93,6 +95,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar", "src\Testcontainers.Pulsar\Testcontainers.Pulsar.csproj", "{27D46863-65B9-4934-B3C8-2383B217A477}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Qdrant", "src\Testcontainers.Qdrant\Testcontainers.Qdrant.csproj", "{7C98973D-53D7-49F9-BDFE-E3268F402584}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq", "src\Testcontainers.RabbitMq\Testcontainers.RabbitMq.csproj", "{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb", "src\Testcontainers.RavenDb\Testcontainers.RavenDb.csproj", "{F6394475-D6F1-46E2-81BF-4BA78A40B878}" @@ -211,6 +215,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub.Tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar.Tests", "tests\Testcontainers.Pulsar.Tests\Testcontainers.Pulsar.Tests.csproj", "{D05FCB31-793E-43E0-BD6C-077013AE9113}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Qdrant.Tests", "tests\Testcontainers.Qdrant.Tests\Testcontainers.Qdrant.Tests.csproj", "{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq.Tests", "tests\Testcontainers.RabbitMq.Tests\Testcontainers.RabbitMq.Tests.csproj", "{19564567-1736-4626-B406-17E4E02F18B2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb.Tests", "tests\Testcontainers.RavenDb.Tests\Testcontainers.RavenDb.Tests.csproj", "{D53726B6-5447-47E6-B881-A44EFF6E5534}" @@ -322,6 +328,10 @@ Global {B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Release|Any CPU.ActiveCfg = Release|Any CPU {B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Release|Any CPU.Build.0 = Release|Any CPU + {D7CE8744-E58B-4277-BBB7-6840A4FF2049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7CE8744-E58B-4277-BBB7-6840A4FF2049}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7CE8744-E58B-4277-BBB7-6840A4FF2049}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7CE8744-E58B-4277-BBB7-6840A4FF2049}.Release|Any CPU.Build.0 = Release|Any CPU {8F483B83-7BD4-4BD5-9F03-DFC26E1CE678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F483B83-7BD4-4BD5-9F03-DFC26E1CE678}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F483B83-7BD4-4BD5-9F03-DFC26E1CE678}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -402,6 +412,10 @@ Global {27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.Build.0 = Debug|Any CPU {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.ActiveCfg = Release|Any CPU {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.Build.0 = Release|Any CPU + {7C98973D-53D7-49F9-BDFE-E3268F402584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C98973D-53D7-49F9-BDFE-E3268F402584}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C98973D-53D7-49F9-BDFE-E3268F402584}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C98973D-53D7-49F9-BDFE-E3268F402584}.Release|Any CPU.Build.0 = Release|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -638,6 +652,10 @@ Global {D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.Build.0 = Debug|Any CPU {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.ActiveCfg = Release|Any CPU {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.Build.0 = Release|Any CPU + {9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Release|Any CPU.Build.0 = Release|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -704,6 +722,7 @@ Global {FF86B509-2F9E-4269-ABC2-912B3339DE29} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {31BAF2C4-0608-4C0F-845A-14FE7C0A1670} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {B3CC460D-0DFD-48A8-9502-54E9828B7B05} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {D7CE8744-E58B-4277-BBB7-6840A4FF2049} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8F483B83-7BD4-4BD5-9F03-DFC26E1CE678} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {C5AF86A8-2F11-41B6-BB01-325AD9016B94} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {111B840F-9DB0-4166-83E6-0580FD418F07} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -724,6 +743,7 @@ Global {8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {E6642255-667D-476B-B584-089AA5E6C0B1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {27D46863-65B9-4934-B3C8-2383B217A477} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {7C98973D-53D7-49F9-BDFE-E3268F402584} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {F6394475-D6F1-46E2-81BF-4BA78A40B878} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -783,6 +803,7 @@ Global {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {D05FCB31-793E-43E0-BD6C-077013AE9113} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {19564567-1736-4626-B406-17E4E02F18B2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {D53726B6-5447-47E6-B881-A44EFF6E5534} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {31EE94A0-E721-4073-B6F1-DD912D004DEF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/compose.yml b/compose.yml new file mode 100644 index 000000000..0f0a55963 --- /dev/null +++ b/compose.yml @@ -0,0 +1,9 @@ +services: + docs: + image: python:3.8-alpine + command: sh -c "pip install -r requirements.txt && mkdocs serve -a 0.0.0.0:8000" + working_dir: /docs + volumes: + - ./:/docs + ports: + - 8000:8000 \ No newline at end of file diff --git a/docs/cicd/index.md b/docs/cicd/index.md index 9cc6b83c5..c7b963086 100644 --- a/docs/cicd/index.md +++ b/docs/cicd/index.md @@ -20,3 +20,23 @@ services: variables: DOCKER_HOST: tcp://docker:2375 ``` + +## Bitbucket Pipelines + +Enable Bitbucket Pipelines as usual on the **Repository settings → Pipelines → Settings** page. After enabling your pipeline, replace the contents of the `bitbucket-pipelines.yml` file, located at the root of your repository, with the following: + +```yml title="bitbucket-pipelines.yml file" +image: mcr.microsoft.com/dotnet/sdk:8.0 +options: + docker: true +pipelines: + default: + - step: + script: + # Bitbucket Pipelines does not support Ryuk: + # https://dotnet.testcontainers.org/api/resource_reaper/. + - export TESTCONTAINERS_RYUK_DISABLED=true + - dotnet test + services: + - docker +``` \ No newline at end of file diff --git a/docs/contributing_docs.md b/docs/contributing_docs.md index 01e809629..e3c67d299 100644 --- a/docs/contributing_docs.md +++ b/docs/contributing_docs.md @@ -15,6 +15,10 @@ We publish our documentation using Netlify. * Set up a virtualenv and run `pip install -r requirements.txt` in the `testcontainers-dotnet` root directory. * Once Python dependencies have been installed, run `mkdocs serve` to start a local auto-updating MkDocs server. +### Using Docker + +The root of the project contains a `compose.yml` file. Simply run `docker compose up` and access the docs at: `http://localhost:8000`. + ### PR preview deployments Note that documentation for pull requests will automatically be published by Netlify as 'deploy preview'. diff --git a/docs/examples/dind.md b/docs/dind/index.md similarity index 90% rename from docs/examples/dind.md rename to docs/dind/index.md index 7ee4fb406..46d68c46a 100644 --- a/docs/examples/dind.md +++ b/docs/dind/index.md @@ -1,6 +1,6 @@ -# Running inside another container +# Running inside a container -## 'Docker wormhole' pattern - Sibling Docker containers +## 'Docker Wormhole' pattern - Sibling Docker containers ### Docker-only example @@ -33,5 +33,5 @@ services: # environment: # - TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal volumes: - - /var/run/docker.sock:/var/run/docker.sock + - /var/run/docker.sock.raw:/var/run/docker.sock ``` diff --git a/docs/full_framework/index.md b/docs/full_framework/index.md new file mode 100644 index 000000000..b0ef02d5b --- /dev/null +++ b/docs/full_framework/index.md @@ -0,0 +1,11 @@ +# .NET Framework + +When working with older versions of the .NET Framework (e.g., .NET Framework 4.x), you may encounter issues related to assembly binding conflicts. These conflicts typically occur when your application requires specific versions of assemblies that are different from the versions being loaded at runtime. + +To resolve these conflicts and ensure the correct versions of assemblies are used, binding redirects are often necessary. Binding redirects allow you to specify which version of an assembly should be used by the runtime, preventing errors and version mismatches during execution. + +Testcontainers for .NET relies on several external dependencies, which may require different versions of assemblies. Legacy applications or projects targeting the full .NET Framework may not automatically resolve these dependencies correctly, and without binding redirects, runtime errors or unexpected behavior may occur. + +In executable .NET Framework projects (such as console apps, web apps, etc.), Visual Studio typically handles binding redirects automatically. However, this is not the case for class libraries or test projects. + +For **test projects**, binding redirects are **not automatically added** by Visual Studio, which means you may need to manually configure them in the `App.config` file (or enable [`AutoGenerateBindingRedirects`](https://learn.microsoft.com/dotnet/framework/configure-apps/redirect-assembly-versions#rely-on-automatic-binding-redirection)). diff --git a/docs/index.md b/docs/index.md index 00f84f424..985ad2a11 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,8 +7,8 @@ dotnet add package Testcontainers ```csharp title="Run the Hello World container" // Create a new instance of a container. var container = new ContainerBuilder() - // Set the image for the container to "testcontainers/helloworld:1.1.0". - .WithImage("testcontainers/helloworld:1.1.0") + // Set the image for the container to "testcontainers/helloworld:1.2.0". + .WithImage("testcontainers/helloworld:1.2.0") // Bind port 8080 of the container to a random port on the host. .WithPortBinding(8080, true) // Wait until the HTTP endpoint of the container is available. @@ -21,7 +21,7 @@ await container.StartAsync() .ConfigureAwait(false); // Create a new instance of HttpClient to send HTTP requests. -var httpClient = new HttpClient(); +using var httpClient = new HttpClient(); // Construct the request URI by specifying the scheme, hostname, assigned random host port, and the endpoint "uuid". var requestUri = new UriBuilder(Uri.UriSchemeHttp, container.Hostname, container.GetMappedPublicPort(8080), "uuid").Uri; diff --git a/docs/modules/activemq.md b/docs/modules/activemq.md new file mode 100644 index 000000000..eba03874f --- /dev/null +++ b/docs/modules/activemq.md @@ -0,0 +1,47 @@ +# Apache ActiveMQ Artemis + +[Apache ActiveMQ Artemis](https://activemq.apache.org/components/artemis/) is an open source project to build a multi-protocol, embeddable, very high performance, clustered, asynchronous messaging system. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.ActiveMq +``` + +You can start an Apache ActiveMQ Artemis 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. + +=== "Base test class" + ```csharp + --8<-- "tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs:UseArtemisContainer" + } + ``` +=== "Without auth" + ```csharp + --8<-- "tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs:UseArtemisContainerNoAuth" + ``` +=== "Default credentials" + ```csharp + --8<-- "tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs:UseArtemisContainerDefaultAuth" + ``` +=== "Custom credentials" + ```csharp + --8<-- "tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs:UseArtemisContainerCustomAuth" + ``` + +Connect to the container and produce a message: + +=== "EstablishesConnection" + ```csharp + --8<-- "tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs:ArtemisContainerEstablishesConnection" + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" + ```xml + --8<-- "tests/Testcontainers.ActiveMq.Tests/Testcontainers.ActiveMq.Tests.csproj:PackageReferences" + ``` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/_call_out_test_projects.txt" \ No newline at end of file diff --git a/docs/modules/cassandra.md b/docs/modules/cassandra.md new file mode 100644 index 000000000..7e5b70abf --- /dev/null +++ b/docs/modules/cassandra.md @@ -0,0 +1,27 @@ +# Apache Cassandra + +[Apache Cassandra](https://cassandra.apache.org/) is a powerful, open-source, distributed NoSQL database that is highly available and fault-tolerant, used to store, manage, and retrieve structured data. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.Cassandra +``` + +You can start an Apache Cassandra 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. + +=== "Usage Example" + ```csharp + --8<-- "tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs:UseCassandraContainer" + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" + ```xml + --8<-- "tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj:PackageReferences" + ``` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/_call_out_test_projects.txt" \ No newline at end of file diff --git a/docs/modules/eventhubs.md b/docs/modules/eventhubs.md new file mode 100644 index 000000000..16662601c --- /dev/null +++ b/docs/modules/eventhubs.md @@ -0,0 +1,43 @@ +# Azure Event Hubs + +Azure [Event Hubs](https://learn.microsoft.com/en-us/azure/event-hubs/overview-emulator) emulator⁠ is designed to offer a local development experience for Azure Event Hubs⁠, enabling you to develop and test code against the service in isolation, free from cloud interference. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.EventHubs +``` + +You can start an Azure Event Hubs container instance from any .NET application. Here, we create different container instances and pass them to the base test class. This allows us to test different configurations. + +=== "Create Container Instance" + ```csharp + --8<-- "tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs:CreateEventHubsContainer" + ``` + +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. + +=== "Usage Example" + ```csharp + --8<-- "tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs:UseEventHubsContainer" + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" + ```xml + --8<-- "tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj:PackageReferences" + ``` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/_call_out_test_projects.txt" + +## Use a custom Azurite instance + +The Event Hubs module depends on an Azurite container instance. The module automatically creates and configures the necessary resources and connects them. If you prefer to use your own instance, you can use the following method to configure the builder accordingly: + +=== "Reuse Existing Resources" + ```csharp + --8<-- "tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs:ReuseExistingAzuriteContainer" + ``` \ No newline at end of file diff --git a/docs/modules/index.md b/docs/modules/index.md index 907472176..f263408cc 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -61,6 +61,7 @@ await moduleNameContainer.StartAsync(); | PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) | | PubSub | `gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators` | [NuGet](https://www.nuget.org/packages/Testcontainers.PubSub) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PubSub) | | Pulsar | `apachepulsar/pulsar:3.0.6` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) | +| Qdrant | `qdrant/qdrant:v1.13.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Qdrant) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Qdrant) | | RabbitMQ | `rabbitmq:3.11` | [NuGet](https://www.nuget.org/packages/Testcontainers.RabbitMq) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RabbitMq) | | RavenDB | `ravendb/ravendb:5.4-ubuntu-latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.RavenDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RavenDb) | | Redis | `redis:7.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Redis) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Redis) | diff --git a/docs/modules/qdrant.md b/docs/modules/qdrant.md new file mode 100644 index 000000000..5de677a2a --- /dev/null +++ b/docs/modules/qdrant.md @@ -0,0 +1,75 @@ +# Qdrant + +[Qdrant](https://qdrant.tech/) is an open source vector database designed for scalable and efficient similarity search and nearest neighbor retrieval. It provides both RESTful and gRPC APIs, making it easy to integrate with various applications, including search, recommendation, AI, and machine learning systems. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.Qdrant +``` + +You can start an Qdrant 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. + +=== "Usage Example" + ```csharp + --8<-- "tests/Testcontainers.Qdrant.Tests/QdrantDefaultContainerTest.cs:UseQdrantContainer" + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" + ```xml + --8<-- "tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj:PackageReferences" + ``` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/_call_out_test_projects.txt" + +## Configure API key + +To set and configure an API key, use the following container builder method: + +=== "Configure the API key" + ```csharp + --8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantContainerApiKey" + ``` + +Make sure the underlying Qdrant HTTP or gRPC client adds the API key to the HTTP header or gRPC metadata: + +=== "Configure the Qdrant client" + ```csharp + --8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantClientApiKey" + ``` + +## Configure TLS + +The following example generates a self-signed certificate and configures the module to use TLS with the certificate and private key: + +!!! note + + Please ensure that both the certificate and private key are provided in PEM format. + +=== "Configure the TLS certificate" + ```csharp + --8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantContainerCertificate" + ``` + +The Qdrant client is configured to validate the TLS certificate using its thumbprint: + +=== "Configure the Qdrant client" + ```csharp + --8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantClientCertificate-1" + + --8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantClientCertificate-2" + ``` + +## A Note To Developers + +The module creates a container that listens to requests over **HTTP**. The official Qdrant client uses the gRPC APIs to communicate with Qdrant. **.NET Core** and **.NET** support the above example with no additional configuration. However, **.NET Framework** has limited supported for gRPC over HTTP/2, but it can be enabled by: + +1. Configuring the module to use TLS. +1. Configuring server certificate validation. +1. Reference [`System.Net.Http.WinHttpHandler`](https://www.nuget.org/packages/System.Net.Http.WinHttpHandler) version `6.0.1` or later, and configure `WinHttpHandler` as the handler for `GrpcChannelOptions` in the Qdrant client. + +Refer to the official [Qdrant .NET SDK](https://github.com/qdrant/qdrant-dotnet) for more information. \ No newline at end of file diff --git a/docs/modules/servicebus.md b/docs/modules/servicebus.md new file mode 100644 index 000000000..8ca30525d --- /dev/null +++ b/docs/modules/servicebus.md @@ -0,0 +1,43 @@ +# Azure Service Bus + +Azure [Service Bus](https://learn.microsoft.com/en-us/azure/service-bus-messaging/overview-emulator) emulator⁠ is designed to offer a local development experience for Azure Service Bus⁠, enabling you to develop and test code against the service in isolation, free from cloud interference. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.ServiceBus +``` + +You can start an Azure Service Bus container instance from any .NET application. Here, we create different container instances and pass them to the base test class. This allows us to test different configurations. + +=== "Create Container Instance" + ```csharp + --8<-- "tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs:CreateServiceBusContainer" + ``` + +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. + +=== "Usage Example" + ```csharp + --8<-- "tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs:UseServiceBusContainer" + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" + ```xml + --8<-- "tests/Testcontainers.ServiceBus.Tests/Testcontainers.ServiceBus.Tests.csproj:PackageReferences" + ``` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/_call_out_test_projects.txt" + +## Use a custom MSSQL instance + +The Service Bus module depends on an MSSQL container instance. The module automatically creates and configures the necessary resources and connects them. If you prefer to use your own instance, you can use the following method to configure the builder accordingly: + +=== "Reuse Existing Resources" + ```csharp + --8<-- "tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs:ReuseExistingMsSqlContainer" + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index f0c491d57..a7d0d403b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,19 +41,25 @@ nav: - api/resource_reuse.md - api/wait_strategies.md - api/best_practices.md + - dind/index.md + - full_framework/index.md - test_frameworks/xunit_net.md - Examples: - - examples/dind.md - examples/aspnet.md - Modules: - modules/index.md + - modules/activemq.md # Apache + - modules/cassandra.md # Apache + - modules/pulsar.md # Apache + - modules/eventhubs.md # Azure + - modules/servicebus.md # Azure - modules/db2.md - modules/elasticsearch.md - modules/mongodb.md - modules/mssql.md - modules/neo4j.md - modules/postgres.md - - modules/pulsar.md + - modules/qdrant.md - modules/rabbitmq.md - contributing.md - contributing_docs.md \ No newline at end of file diff --git a/src/Testcontainers.Db2/Db2Container.cs b/src/Testcontainers.Db2/Db2Container.cs index a52dd5642..660a388d1 100644 --- a/src/Testcontainers.Db2/Db2Container.cs +++ b/src/Testcontainers.Db2/Db2Container.cs @@ -42,7 +42,7 @@ public async Task ExecScriptAsync(string scriptContent, Cancellation await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct) .ConfigureAwait(false); - return await ExecAsync(new [] { "/bin/sh", "-c", db2ShellCommand}, ct) + return await ExecAsync(new[] { "/bin/sh", "-c", db2ShellCommand}, ct) .ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/EventHubsBuilder.cs b/src/Testcontainers.EventHubs/EventHubsBuilder.cs index 3e8fb7476..87b207531 100644 --- a/src/Testcontainers.EventHubs/EventHubsBuilder.cs +++ b/src/Testcontainers.EventHubs/EventHubsBuilder.cs @@ -12,6 +12,8 @@ public sealed class EventHubsBuilder : ContainerBuilder /// Initializes a new instance of the class. /// @@ -135,9 +137,9 @@ protected override EventHubsBuilder Init() return base.Init() .WithImage(EventHubsImage) .WithPortBinding(EventHubsPort, true) - .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged("Emulator Service is Successfully Up!") - .AddCustomWaitStrategy(new WaitTwoSeconds())); + .WithPortBinding(EventHubsHttpPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPort(EventHubsHttpPort).ForPath("/health"))); } /// @@ -157,22 +159,4 @@ protected override EventHubsBuilder Merge(EventHubsConfiguration oldValue, Event { return new EventHubsBuilder(new EventHubsConfiguration(oldValue, newValue)); } - - /// - /// - /// This is a workaround to ensure that the wait strategy does not indicate - /// readiness too early: - /// https://github.com/Azure/azure-service-bus-emulator-installer/issues/35#issuecomment-2497164533. - /// - private sealed class WaitTwoSeconds : IWaitUntil - { - /// - public async Task UntilAsync(IContainer container) - { - await Task.Delay(TimeSpan.FromSeconds(2)) - .ConfigureAwait(false); - - return true; - } - } } \ No newline at end of file diff --git a/src/Testcontainers.GCloud/.editorconfig b/src/Testcontainers.GCloud/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.GCloud/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.GCloud/Testcontainers.GCloud.csproj b/src/Testcontainers.GCloud/Testcontainers.GCloud.csproj new file mode 100644 index 000000000..2c527d992 --- /dev/null +++ b/src/Testcontainers.GCloud/Testcontainers.GCloud.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 1f255cd3a..d3d406dc3 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -44,6 +44,7 @@ private KeycloakBuilder(KeycloakConfiguration resourceConfiguration) public KeycloakBuilder WithUsername(string username) { return Merge(DockerResourceConfiguration, new KeycloakConfiguration(username: username)) + .WithEnvironment("KC_BOOTSTRAP_ADMIN_USERNAME", username) .WithEnvironment("KEYCLOAK_ADMIN", username); } @@ -55,6 +56,7 @@ public KeycloakBuilder WithUsername(string username) public KeycloakBuilder WithPassword(string password) { return Merge(DockerResourceConfiguration, new KeycloakConfiguration(password: password)) + .WithEnvironment("KC_BOOTSTRAP_ADMIN_PASSWORD", password) .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", password); } diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 173690c6f..23218b1ea 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -68,13 +68,20 @@ public override PulsarContainer Build() { Validate(); - var waitStrategy = Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration.AuthenticationEnabled.GetValueOrDefault())); + var waitStrategy = Wait.ForUnixContainer(); + + if (DockerResourceConfiguration.AuthenticationEnabled.GetValueOrDefault()) + { + waitStrategy = waitStrategy.UntilFileExists(SecretKeyFilePath, FileSystem.Container); + } if (DockerResourceConfiguration.FunctionsWorkerEnabled.GetValueOrDefault()) { waitStrategy = waitStrategy.UntilMessageIsLogged("Function worker service started"); } + waitStrategy = waitStrategy.AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration.AuthenticationEnabled.GetValueOrDefault())); + var pulsarBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy); return new PulsarContainer(pulsarBuilder.DockerResourceConfiguration); } diff --git a/src/Testcontainers.Qdrant/.editorconfig b/src/Testcontainers.Qdrant/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Qdrant/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/QdrantBuilder.cs b/src/Testcontainers.Qdrant/QdrantBuilder.cs new file mode 100644 index 000000000..703119346 --- /dev/null +++ b/src/Testcontainers.Qdrant/QdrantBuilder.cs @@ -0,0 +1,134 @@ +namespace Testcontainers.Qdrant; + +/// +[PublicAPI] +public sealed class QdrantBuilder : ContainerBuilder +{ + public const string QdrantImage = "qdrant/qdrant:v1.13.4"; + + public const ushort QdrantHttpPort = 6333; + + public const ushort QdrantGrpcPort = 6334; + + public const string CertificateFilePath = "/qdrant/tls/cert.pem"; + + public const string CertificateKeyFilePath = "/qdrant/tls/key.pem"; + + /// + /// Initializes a new instance of the class. + /// + public QdrantBuilder() + : this(new QdrantConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private QdrantBuilder(QdrantConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override QdrantConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the API key to secure the instance. + /// + /// The API key. + /// A configured instance of . + public QdrantBuilder WithApiKey(string apiKey) + { + return Merge(DockerResourceConfiguration, new QdrantConfiguration(apiKey: apiKey)) + .WithEnvironment("QDRANT__SERVICE__API_KEY", apiKey); + } + + /// + /// Sets the public certificate and private key to enable TLS. + /// + /// The public certificate in PEM format. + /// The private key associated with the certificate in PEM format. + /// A configured instance of . + public QdrantBuilder WithCertificate(string certificate, string certificateKey) + { + return Merge(DockerResourceConfiguration, new QdrantConfiguration(certificate: certificate, certificateKey: certificateKey)) + .WithEnvironment("QDRANT__SERVICE__ENABLE_TLS", "1") + .WithEnvironment("QDRANT__TLS__CERT", CertificateFilePath) + .WithEnvironment("QDRANT__TLS__KEY", CertificateKeyFilePath) + .WithResourceMapping(Encoding.UTF8.GetBytes(certificate), CertificateFilePath) + .WithResourceMapping(Encoding.UTF8.GetBytes(certificateKey), CertificateKeyFilePath); + } + + /// + public override QdrantContainer Build() + { + Validate(); + + // By default, the base builder waits until the container is running. However, for Qdrant, a more advanced waiting strategy is necessary that requires access to the configured certificate. + // If the user does not provide a custom waiting strategy, append the default Qdrant waiting strategy. + var qdrantBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration))); + return new QdrantContainer(qdrantBuilder.DockerResourceConfiguration); + } + + /// + protected override QdrantBuilder Init() + { + return base.Init() + .WithImage(QdrantImage) + .WithPortBinding(QdrantHttpPort, true) + .WithPortBinding(QdrantGrpcPort, true); + } + + /// + protected override QdrantBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); + } + + /// + protected override QdrantBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); + } + + /// + protected override QdrantBuilder Merge(QdrantConfiguration oldValue, QdrantConfiguration newValue) + { + return new QdrantBuilder(new QdrantConfiguration(oldValue, newValue)); + } + + /// + private sealed class WaitUntil : IWaitUntil + { + private readonly bool _tlsEnabled; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public WaitUntil(QdrantConfiguration configuration) + { + _tlsEnabled = configuration.TlsEnabled; + } + + /// + public async Task UntilAsync(IContainer container) + { + using var httpMessageHandler = new HttpClientHandler(); + httpMessageHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + + var httpWaitStrategy = new HttpWaitStrategy() + .UsingHttpMessageHandler(httpMessageHandler) + .UsingTls(_tlsEnabled) + .ForPort(QdrantHttpPort) + .ForPath("/readyz"); + + return await httpWaitStrategy.UntilAsync(container) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/QdrantConfiguration.cs b/src/Testcontainers.Qdrant/QdrantConfiguration.cs new file mode 100644 index 000000000..99a4288d0 --- /dev/null +++ b/src/Testcontainers.Qdrant/QdrantConfiguration.cs @@ -0,0 +1,85 @@ +namespace Testcontainers.Qdrant; + +/// +[PublicAPI] +public sealed class QdrantConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The API key. + /// The public certificate in PEM format. + /// The private key associated with the certificate in PEM format. + public QdrantConfiguration( + string apiKey = null, + string certificate = null, + string certificateKey = null) + { + ApiKey = apiKey; + Certificate = certificate; + CertificateKey = certificateKey; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public QdrantConfiguration(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 QdrantConfiguration(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 QdrantConfiguration(QdrantConfiguration resourceConfiguration) + : this(new QdrantConfiguration(), 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 QdrantConfiguration(QdrantConfiguration oldValue, QdrantConfiguration newValue) + : base(oldValue, newValue) + { + ApiKey = BuildConfiguration.Combine(oldValue.ApiKey, newValue.ApiKey); + Certificate = BuildConfiguration.Combine(oldValue.Certificate, newValue.Certificate); + CertificateKey = BuildConfiguration.Combine(oldValue.CertificateKey, newValue.CertificateKey); + } + + /// + /// Gets a value indicating whether TLS is enabled or not. + /// + public bool TlsEnabled => Certificate != null; + + /// + /// Gets the API key that secures the instance. + /// + public string ApiKey { get; } + + /// + /// Gets the public certificate in PEM format. + /// + public string Certificate { get; } + + /// + /// Gets the private key associated with the certificate in PEM format. + /// + public string CertificateKey { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/QdrantContainer.cs b/src/Testcontainers.Qdrant/QdrantContainer.cs new file mode 100644 index 000000000..f6399ee44 --- /dev/null +++ b/src/Testcontainers.Qdrant/QdrantContainer.cs @@ -0,0 +1,38 @@ +namespace Testcontainers.Qdrant; + +/// +[PublicAPI] +public sealed class QdrantContainer : DockerContainer +{ + private readonly QdrantConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public QdrantContainer(QdrantConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + + /// + /// Gets the connection string for connecting to Qdrant REST APIs. + /// + public string GetHttpConnectionString() + { + var scheme = _configuration.TlsEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantHttpPort)); + return endpoint.ToString(); + } + + /// + /// Gets the connection string for connecting to Qdrant gRPC APIs. + /// + public string GetGrpcConnectionString() + { + var scheme = _configuration.TlsEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantGrpcPort)); + return endpoint.ToString(); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj new file mode 100644 index 000000000..cbff16956 --- /dev/null +++ b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1;net462 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/Usings.cs b/src/Testcontainers.Qdrant/Usings.cs new file mode 100644 index 000000000..9c8bc5fba --- /dev/null +++ b/src/Testcontainers.Qdrant/Usings.cs @@ -0,0 +1,10 @@ +global using System; +global using System.Linq; +global using System.Net.Http; +global using System.Text; +global using System.Threading.Tasks; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; \ No newline at end of file diff --git a/src/Testcontainers.ServiceBus/ServiceBusBuilder.cs b/src/Testcontainers.ServiceBus/ServiceBusBuilder.cs index 2d6b0b930..1069802d3 100644 --- a/src/Testcontainers.ServiceBus/ServiceBusBuilder.cs +++ b/src/Testcontainers.ServiceBus/ServiceBusBuilder.cs @@ -12,6 +12,8 @@ public sealed class ServiceBusBuilder : ContainerBuilder /// Initializes a new instance of the class. /// @@ -115,9 +117,10 @@ protected override ServiceBusBuilder Init() return base.Init() .WithImage(ServiceBusImage) .WithPortBinding(ServiceBusPort, true) - .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged("Emulator Service is Successfully Up!") - .AddCustomWaitStrategy(new WaitTwoSeconds())); + .WithPortBinding(ServiceBusHttpPort, true) + .WithEnvironment("SQL_WAIT_INTERVAL", "0") + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPort(ServiceBusHttpPort).ForPath("/health"))); } /// @@ -137,22 +140,4 @@ protected override ServiceBusBuilder Merge(ServiceBusConfiguration oldValue, Ser { return new ServiceBusBuilder(new ServiceBusConfiguration(oldValue, newValue)); } - - /// - /// - /// This is a workaround to ensure that the wait strategy does not indicate - /// readiness too early: - /// https://github.com/Azure/azure-service-bus-emulator-installer/issues/35#issuecomment-2497164533. - /// - private sealed class WaitTwoSeconds : IWaitUntil - { - /// - public async Task UntilAsync(IContainer container) - { - await Task.Delay(TimeSpan.FromSeconds(2)) - .ConfigureAwait(false); - - return true; - } - } } \ No newline at end of file diff --git a/src/Testcontainers/Builders/BuildConfiguration.cs b/src/Testcontainers/Builders/BuildConfiguration.cs index b96a71c48..824008646 100644 --- a/src/Testcontainers/Builders/BuildConfiguration.cs +++ b/src/Testcontainers/Builders/BuildConfiguration.cs @@ -27,7 +27,6 @@ public static T Combine(T oldValue, T newValue) /// Type of . /// An updated configuration. public static IEnumerable Combine(IEnumerable oldValue, IEnumerable newValue) - where T : class { if (newValue == null && oldValue == null) { @@ -51,7 +50,6 @@ public static IEnumerable Combine(IEnumerable oldValue, IEnumerable /// Type of . /// An updated configuration. public static IReadOnlyList Combine(IReadOnlyList oldValue, IReadOnlyList newValue) - where T : class { if (newValue == null && oldValue == null) { @@ -75,8 +73,6 @@ public static IReadOnlyList Combine(IReadOnlyList oldValue, IReadOnlyLi /// The type of values in the read-only dictionary. /// An updated configuration. public static IReadOnlyDictionary Combine(IReadOnlyDictionary oldValue, IReadOnlyDictionary newValue) - where TKey : class - where TValue : class { if (newValue == null && oldValue == null) { diff --git a/src/Testcontainers/Builders/ContainerBuilder`3.cs b/src/Testcontainers/Builders/ContainerBuilder`3.cs index 8bf63d77a..468c894c9 100644 --- a/src/Testcontainers/Builders/ContainerBuilder`3.cs +++ b/src/Testcontainers/Builders/ContainerBuilder`3.cs @@ -92,17 +92,7 @@ public TBuilderEntity WithImage(string image) /// public TBuilderEntity WithImage(IImage image) { - if (string.IsNullOrEmpty(TestcontainersSettings.HubImageNamePrefix)) - { - return Clone(new ContainerConfiguration(image: image)); - } - - if (!string.IsNullOrEmpty(image.GetHostname())) - { - return Clone(new ContainerConfiguration(image: image)); - } - - return Clone(new ContainerConfiguration(image: new DockerImage(image.Repository, image.Registry, image.Tag, image.Digest, TestcontainersSettings.HubImageNamePrefix))); + return Clone(new ContainerConfiguration(image: image.ApplyHubImageNamePrefix())); } /// diff --git a/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs b/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs index 47f2d7131..d8be6544a 100644 --- a/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs +++ b/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs @@ -23,10 +23,10 @@ public interface IImageFromDockerfileBuilder /// /// Sets the name. /// - /// The name. + /// The image. /// A configured instance of . [PublicAPI] - TBuilderEntity WithName(IImage name); + TBuilderEntity WithName(IImage image); /// /// Sets the Dockerfile. diff --git a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs index 5a4eea426..7ddc6e779 100644 --- a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs +++ b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs @@ -58,9 +58,9 @@ public ImageFromDockerfileBuilder WithName(string name) } /// - public ImageFromDockerfileBuilder WithName(IImage name) + public ImageFromDockerfileBuilder WithName(IImage image) { - return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(image: name)); + return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(image: image.ApplyHubImageNamePrefix())); } /// diff --git a/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs b/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs index 4d988bb71..ed2f89b46 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/UntilMessageIsLogged.cs @@ -21,7 +21,8 @@ public UntilMessageIsLogged(Regex pattern) public async Task UntilAsync(IContainer container) { - var (stdout, stderr) = await container.GetLogsAsync(since: container.StoppedTime, timestampsEnabled: false) + var maxTime = container.StoppedTime > container.CreatedTime ? container.StoppedTime : container.CreatedTime; + var (stdout, stderr) = await container.GetLogsAsync(since: maxTime, timestampsEnabled: false) .ConfigureAwait(false); return _pattern.IsMatch(stdout) || _pattern.IsMatch(stderr); diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs index b1ea3907c..c916f12ec 100644 --- a/src/Testcontainers/Containers/DockerContainer.cs +++ b/src/Testcontainers/Containers/DockerContainer.cs @@ -505,7 +505,7 @@ await _client.AttachAsync(_container.ID, _configuration.OutputConsumer, ct) await _client.StartAsync(_container.ID, ct) .ConfigureAwait(false); - _ = await CheckReadinessAsync(new [] { portBindingsMapped }, ct) + _ = await CheckReadinessAsync(new[] { portBindingsMapped }, ct) .ConfigureAwait(false); Starting?.Invoke(this, EventArgs.Empty); diff --git a/src/Testcontainers/Containers/SocatBuilder.cs b/src/Testcontainers/Containers/SocatBuilder.cs new file mode 100644 index 000000000..fef22f990 --- /dev/null +++ b/src/Testcontainers/Containers/SocatBuilder.cs @@ -0,0 +1,118 @@ +namespace DotNet.Testcontainers.Containers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Docker.DotNet.Models; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + + /// + [PublicAPI] + public sealed class SocatBuilder : ContainerBuilder + { + public const string SocatImage = "alpine/socat:1.7.4.3-r0"; + + /// + /// Initializes a new instance of the class. + /// + public SocatBuilder() + : this(new SocatConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private SocatBuilder(SocatConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override SocatConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Socat target. + /// + /// The Socat exposed port. + /// The Socat target host. + /// A configured instance of . + public SocatBuilder WithTarget(int exposedPort, string host) + { + return WithTarget(exposedPort, host, exposedPort); + } + + /// + /// Sets the Socat target. + /// + /// The Socat exposed port. + /// The Socat target host. + /// The Socat target port. + /// A configured instance of . + public SocatBuilder WithTarget(int exposedPort, string host, int internalPort) + { + var targets = new Dictionary { { exposedPort, $"{host}:{internalPort}" } }; + return Merge(DockerResourceConfiguration, new SocatConfiguration(targets)) + .WithPortBinding(exposedPort, true); + } + + /// + public override SocatContainer Build() + { + Validate(); + + const string argument = "socat TCP-LISTEN:{0},fork,reuseaddr TCP:{1}"; + + var command = string.Join(" & ", DockerResourceConfiguration.Targets + .Select(item => string.Format(argument, item.Key, item.Value))); + + var waitStrategy = DockerResourceConfiguration.Targets + .Aggregate(Wait.ForUnixContainer(), (waitStrategy, item) => waitStrategy.UntilPortIsAvailable(item.Key)); + + var socatBuilder = WithCommand(command).WithWaitStrategy(waitStrategy); + return new SocatContainer(socatBuilder.DockerResourceConfiguration); + } + + /// + protected override SocatBuilder Init() + { + return base.Init() + .WithImage(SocatImage) + .WithEntrypoint("/bin/sh", "-c"); + } + + /// + protected override void Validate() + { + const string message = "Missing targets. One target must be specified to be created."; + + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.Targets, nameof(DockerResourceConfiguration.Targets)) + .ThrowIf(argument => argument.Value.Count == 0, argument => new ArgumentException(message, argument.Name)); + } + + /// + protected override SocatBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SocatConfiguration(resourceConfiguration)); + } + + /// + protected override SocatBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SocatConfiguration(resourceConfiguration)); + } + + /// + protected override SocatBuilder Merge(SocatConfiguration oldValue, SocatConfiguration newValue) + { + return new SocatBuilder(new SocatConfiguration(oldValue, newValue)); + } + } +} diff --git a/src/Testcontainers/Containers/SocatConfiguration.cs b/src/Testcontainers/Containers/SocatConfiguration.cs new file mode 100644 index 000000000..e1f9c0384 --- /dev/null +++ b/src/Testcontainers/Containers/SocatConfiguration.cs @@ -0,0 +1,69 @@ +namespace DotNet.Testcontainers.Containers +{ + using System.Collections.Generic; + using Docker.DotNet.Models; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + + /// + [PublicAPI] + public sealed class SocatConfiguration : ContainerConfiguration + { + /// + /// Initializes a new instance of the class. + /// + /// A list of target addresses. + public SocatConfiguration( + IReadOnlyDictionary targets = null) + { + Targets = targets; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SocatConfiguration(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 SocatConfiguration(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 SocatConfiguration(SocatConfiguration resourceConfiguration) + : this(new SocatConfiguration(), 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 SocatConfiguration(SocatConfiguration oldValue, SocatConfiguration newValue) + : base(oldValue, newValue) + { + Targets = BuildConfiguration.Combine(oldValue.Targets, newValue.Targets); + } + + /// + /// Gets a list of target addresses. + /// + public IReadOnlyDictionary Targets { get; } + } +} diff --git a/src/Testcontainers/Containers/SocatContainer.cs b/src/Testcontainers/Containers/SocatContainer.cs new file mode 100644 index 000000000..027672d49 --- /dev/null +++ b/src/Testcontainers/Containers/SocatContainer.cs @@ -0,0 +1,21 @@ +namespace DotNet.Testcontainers.Containers +{ + using JetBrains.Annotations; + + /// + [PublicAPI] + public sealed class SocatContainer : DockerContainer + { + private readonly SocatConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public SocatContainer(SocatConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + } +} diff --git a/src/Testcontainers/Images/DockerfileArchive.cs b/src/Testcontainers/Images/DockerfileArchive.cs index c28a6914e..500d49bcb 100644 --- a/src/Testcontainers/Images/DockerfileArchive.cs +++ b/src/Testcontainers/Images/DockerfileArchive.cs @@ -4,7 +4,6 @@ namespace DotNet.Testcontainers.Images using System.Collections.Generic; using System.IO; using System.Linq; - using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -94,7 +93,7 @@ public IEnumerable GetBaseImages() var stages = lines .Select(line => line.Value) - .Select(line => line.Split(new [] { " AS ", " As ", " aS ", " as " }, StringSplitOptions.RemoveEmptyEntries)) + .Select(line => line.Split(new[] { " AS ", " As ", " aS ", " as " }, StringSplitOptions.RemoveEmptyEntries)) .Where(substrings => substrings.Length > 1) .Select(substrings => substrings[substrings.Length - 1]) .Distinct() @@ -150,11 +149,7 @@ public async Task Tar(CancellationToken ct = default) { var entry = TarEntry.CreateTarEntry(relativeFilePath); entry.TarHeader.Size = inputStream.Length; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - entry.TarHeader.Mode = (int)Unix.FileMode755; - } + entry.TarHeader.Mode = GetUnixFileMode(absoluteFilePath); await tarOutputStream.PutNextEntryAsync(entry, ct) .ConfigureAwait(false); @@ -190,5 +185,25 @@ private static IEnumerable GetFiles(string directory) .Select(Unix.Instance.NormalizePath) .ToArray(); } + + /// + /// Gets the Unix file mode of the file on the path. + /// + /// The path to the file. + /// The Unix file mode of the file on the path. + private static int GetUnixFileMode(string filePath) + { +#if NET7_0_OR_GREATER + if (!OperatingSystem.IsWindows()) + { + return (int)File.GetUnixFileMode(filePath); + } +#endif + + // Default to 755 for Windows and fall back to 755 for Unix when `GetUnixFileMode` + // is not available. + _ = filePath; + return (int)Unix.FileMode755; + } } } diff --git a/src/Testcontainers/Images/IImageExtensions.cs b/src/Testcontainers/Images/IImageExtensions.cs new file mode 100644 index 000000000..bbfde1ce0 --- /dev/null +++ b/src/Testcontainers/Images/IImageExtensions.cs @@ -0,0 +1,33 @@ +namespace DotNet.Testcontainers.Images +{ + using DotNet.Testcontainers.Configurations; + + /// + /// Provides extension methods for the interface. + /// + internal static class IImageExtensions + { + /// + /// Applies the Docker Hub image name prefix if it is configured. + /// + /// The original instance. + /// + /// A new instance with the Docker Hub image name prefix + /// applied, or the original instance if no prefix is set. + /// + public static IImage ApplyHubImageNamePrefix(this IImage image) + { + if (string.IsNullOrEmpty(TestcontainersSettings.HubImageNamePrefix)) + { + return image; + } + + if (!string.IsNullOrEmpty(image.GetHostname())) + { + return image; + } + + return new DockerImage(image.Repository, image.Registry, image.Tag, image.Digest, TestcontainersSettings.HubImageNamePrefix); + } + } +} diff --git a/tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs b/tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs index 704e44f68..baafb31d8 100644 --- a/tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs +++ b/tests/Testcontainers.ActiveMq.Tests/ArtemisContainerTest.cs @@ -1,5 +1,6 @@ namespace Testcontainers.ActiveMq; +// # --8<-- [start:UseArtemisContainer] public abstract class ArtemisContainerTest : IAsyncLifetime { private readonly ArtemisContainer _artemisContainer; @@ -24,7 +25,9 @@ public Task DisposeAsync() { return _artemisContainer.DisposeAsync().AsTask(); } + // # --8<-- [end:UseArtemisContainer] + // # --8<-- [start:ArtemisContainerEstablishesConnection] [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] public async Task EstablishesConnection() @@ -67,7 +70,9 @@ await producer.SendAsync(producedMessage) Assert.Equal(producedMessage.Text, receivedMessage.Body()); } + // # --8<-- [end:ArtemisContainerEstablishesConnection] + // # --8<-- [start:UseArtemisContainerDefaultAuth] [UsedImplicitly] public sealed class DefaultCredentialsConfiguration : ArtemisContainerTest { @@ -76,7 +81,9 @@ public DefaultCredentialsConfiguration() { } } + // # --8<-- [end:UseArtemisContainerDefaultAuth] + // # --8<-- [start:UseArtemisContainerCustomAuth] [UsedImplicitly] public sealed class CustomCredentialsConfiguration : ArtemisContainerTest { @@ -89,7 +96,9 @@ public CustomCredentialsConfiguration() { } } + // # --8<-- [end:UseArtemisContainerCustomAuth] + // # --8<-- [start:UseArtemisContainerNoAuth] [UsedImplicitly] public sealed class NoAuthCredentialsConfiguration : ArtemisContainerTest { @@ -98,4 +107,5 @@ public NoAuthCredentialsConfiguration() { } } + // # --8<-- [end:UseArtemisContainerNoAuth] } \ No newline at end of file diff --git a/tests/Testcontainers.ActiveMq.Tests/Testcontainers.ActiveMq.Tests.csproj b/tests/Testcontainers.ActiveMq.Tests/Testcontainers.ActiveMq.Tests.csproj index 54c6102e3..3f600a5ff 100644 --- a/tests/Testcontainers.ActiveMq.Tests/Testcontainers.ActiveMq.Tests.csproj +++ b/tests/Testcontainers.ActiveMq.Tests/Testcontainers.ActiveMq.Tests.csproj @@ -5,11 +5,13 @@ false + + diff --git a/tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs b/tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs index a871cb368..e31915dcb 100644 --- a/tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs +++ b/tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs @@ -2,6 +2,7 @@ namespace Testcontainers.Cassandra; public sealed class CassandraContainerTest : IAsyncLifetime { + // # --8<-- [start:UseCassandraContainer] private readonly CassandraContainer _cassandraContainer = new CassandraBuilder().Build(); public Task InitializeAsync() @@ -50,6 +51,7 @@ public void ExecuteCqlStatementReturnsExpectedResult() Assert.Single(rows); Assert.Equal("COMPLETED", rows[0]["bootstrapped"]); } + // # --8<-- [end:UseCassandraContainer] [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] diff --git a/tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj b/tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj index 4cd6fb727..6e7c4262b 100644 --- a/tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj +++ b/tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj @@ -5,11 +5,13 @@ false + + diff --git a/tests/Testcontainers.Commons/CommonImages.cs b/tests/Testcontainers.Commons/CommonImages.cs index 22a036285..c58e57283 100644 --- a/tests/Testcontainers.Commons/CommonImages.cs +++ b/tests/Testcontainers.Commons/CommonImages.cs @@ -5,6 +5,8 @@ public static class CommonImages { public static readonly IImage Ryuk = new DockerImage("testcontainers/ryuk:0.9.0"); + public static readonly IImage HelloWorld = new DockerImage("testcontainers/helloworld:1.2.0"); + public static readonly IImage Alpine = new DockerImage("alpine:3.17"); public static readonly IImage Socat = new DockerImage("alpine/socat:1.8.0.0"); diff --git a/tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs b/tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs index 619c5f642..bebbf0bb9 100644 --- a/tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs +++ b/tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs @@ -13,6 +13,7 @@ private EventHubsContainerTest(EventHubsContainer eventHubsContainer) _eventHubsContainer = eventHubsContainer; } + // # --8<-- [start:UseEventHubsContainer] public Task InitializeAsync() { return _eventHubsContainer.StartAsync(); @@ -52,7 +53,9 @@ await client.SendAsync(eventDataBatch) // Then Assert.NotNull(properties); } + // # --8<-- [end:UseEventHubsContainer] + // # --8<-- [start:CreateEventHubsContainer] [UsedImplicitly] public sealed class EventHubsDefaultAzuriteConfiguration : EventHubsContainerTest { @@ -64,6 +67,7 @@ public EventHubsDefaultAzuriteConfiguration() { } } + // # --8<-- [end:CreateEventHubsContainer] [UsedImplicitly] public sealed class EventHubsCustomAzuriteConfiguration : EventHubsContainerTest, IClassFixture @@ -72,7 +76,9 @@ public EventHubsCustomAzuriteConfiguration(DatabaseFixture fixture) : base(new EventHubsBuilder() .WithAcceptLicenseAgreement(true) .WithConfigurationBuilder(GetServiceConfiguration()) + // # --8<-- [start:ReuseExistingAzuriteContainer] .WithAzuriteContainer(fixture.Network, fixture.Container, DatabaseFixture.AzuriteNetworkAlias) + // # --8<-- [end:ReuseExistingAzuriteContainer] .Build()) { } diff --git a/tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj b/tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj index f797bf3b5..e59ed8c18 100644 --- a/tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj +++ b/tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj @@ -5,11 +5,13 @@ false + + diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs index 1aac0872f..d5d3edb06 100644 --- a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs +++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs @@ -65,4 +65,13 @@ public KeycloakV25Configuration() { } } + + [UsedImplicitly] + public sealed class KeycloakV26Configuration : KeycloakContainerTest + { + public KeycloakV26Configuration() + : base(new KeycloakBuilder().WithImage("quay.io/keycloak/keycloak:26.0").Build()) + { + } + } } \ No newline at end of file diff --git a/tests/Testcontainers.Platform.Linux.Tests/SocatContainerTest.cs b/tests/Testcontainers.Platform.Linux.Tests/SocatContainerTest.cs new file mode 100644 index 000000000..879aced43 --- /dev/null +++ b/tests/Testcontainers.Platform.Linux.Tests/SocatContainerTest.cs @@ -0,0 +1,71 @@ +namespace Testcontainers.Tests; + +public sealed class SocatContainerTest : IAsyncLifetime +{ + private const string HelloWorldAlias = "hello-world-container"; + + private readonly INetwork _network; + + private readonly IContainer _helloWorldContainer; + + private readonly IContainer _socatContainer; + + public SocatContainerTest() + { + _network = new NetworkBuilder() + .Build(); + + _helloWorldContainer = new ContainerBuilder() + .WithImage(CommonImages.HelloWorld) + .WithNetwork(_network) + .WithNetworkAliases(HelloWorldAlias) + .Build(); + + _socatContainer = new SocatBuilder() + .WithNetwork(_network) + .WithTarget(8080, HelloWorldAlias) + .WithTarget(8081, HelloWorldAlias, 8080) + .Build(); + } + + public async Task InitializeAsync() + { + await _helloWorldContainer.StartAsync() + .ConfigureAwait(false); + + await _socatContainer.StartAsync() + .ConfigureAwait(false); + } + + public async Task DisposeAsync() + { + await _socatContainer.DisposeAsync() + .ConfigureAwait(false); + + await _helloWorldContainer.DisposeAsync() + .ConfigureAwait(false); + + await _network.DisposeAsync() + .ConfigureAwait(false); + } + + [Theory] + [InlineData(8080)] + [InlineData(8081)] + public async Task RequestTargetContainer(int containerPort) + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new UriBuilder(Uri.UriSchemeHttp, _socatContainer.Hostname, _socatContainer.GetMappedPublicPort(containerPort)).Uri; + + // When + using var httpResponse = await httpClient.GetAsync("/ping") + .ConfigureAwait(true); + + var response = await httpResponse.Content.ReadAsStringAsync() + .ConfigureAwait(true); + + // Then + Assert.Equal("PONG", response); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Platform.Linux.Tests/Usings.cs b/tests/Testcontainers.Platform.Linux.Tests/Usings.cs index ea641a068..89d5592ac 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/Usings.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/Usings.cs @@ -4,6 +4,7 @@ global using System.IO; global using System.Linq; global using System.Net; +global using System.Net.Http; global using System.Net.Sockets; global using System.Text; global using System.Threading; @@ -16,6 +17,7 @@ global using DotNet.Testcontainers.Commons; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; +global using DotNet.Testcontainers.Networks; global using ICSharpCode.SharpZipLib.Tar; global using JetBrains.Annotations; global using Microsoft.Extensions.Logging.Abstractions; diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs index 21af79639..bb0405417 100644 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs @@ -2,6 +2,8 @@ namespace Testcontainers.Pulsar; public abstract class PulsarContainerTest : IAsyncLifetime { + private static readonly IReadOnlyDictionary MemorySettings = new Dictionary { { "PULSAR_MEM", "-Xms256m -Xmx512m" } }; + private readonly PulsarContainer _pulsarContainer; private readonly bool _authenticationEnabled; @@ -69,7 +71,9 @@ public async Task ConsumerReceivesSendMessage() public sealed class PulsarDefaultConfiguration : PulsarContainerTest { public PulsarDefaultConfiguration() - : base(new PulsarBuilder().Build(), false) + : base(new PulsarBuilder() + .WithEnvironment(MemorySettings) + .Build(), false) { } } @@ -79,7 +83,10 @@ public PulsarDefaultConfiguration() public sealed class PulsarAuthConfiguration : PulsarContainerTest { public PulsarAuthConfiguration() - : base(new PulsarBuilder().WithAuthentication().Build(), true) + : base(new PulsarBuilder() + .WithAuthentication() + .WithEnvironment(MemorySettings) + .Build(), true) { } } @@ -88,7 +95,10 @@ public PulsarAuthConfiguration() public sealed class PulsarV4Configuration : PulsarContainerTest { public PulsarV4Configuration() - : base(new PulsarBuilder().WithImage("apachepulsar/pulsar:4.0.2").Build(), false) + : base(new PulsarBuilder() + .WithImage("apachepulsar/pulsar:4.0.2") + .WithEnvironment(MemorySettings) + .Build(), false) { } } @@ -97,7 +107,11 @@ public PulsarV4Configuration() public sealed class PulsarV4AuthConfiguration : PulsarContainerTest { public PulsarV4AuthConfiguration() - : base(new PulsarBuilder().WithImage("apachepulsar/pulsar:4.0.2").WithAuthentication().Build(), true) + : base(new PulsarBuilder() + .WithImage("apachepulsar/pulsar:4.0.2") + .WithAuthentication() + .WithEnvironment(MemorySettings) + .Build(), true) { } } diff --git a/tests/Testcontainers.Pulsar.Tests/Usings.cs b/tests/Testcontainers.Pulsar.Tests/Usings.cs index 4eb245799..ad94dd31c 100644 --- a/tests/Testcontainers.Pulsar.Tests/Usings.cs +++ b/tests/Testcontainers.Pulsar.Tests/Usings.cs @@ -1,4 +1,5 @@ global using System; +global using System.Collections.Generic; global using System.Text; global using System.Threading; global using System.Threading.Tasks; diff --git a/tests/Testcontainers.Qdrant.Tests/.editorconfig b/tests/Testcontainers.Qdrant.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs b/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs new file mode 100644 index 000000000..49ee96a43 --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs @@ -0,0 +1,35 @@ +namespace Testcontainers.Qdrant; + +public sealed class PemCertificate +{ + static PemCertificate() + { + } + + private PemCertificate(string commonName) + { + using var rsa = RSA.Create(2048); + + var subjectName = new X500DistinguishedName($"CN={commonName}"); + + var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + using var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + + CommonName = commonName; + Thumbprint = certificate.GetCertHashString(HashAlgorithmName.SHA256); + Certificate = certificate.ExportCertificatePem(); + CertificateKey = rsa.ExportPkcs8PrivateKeyPem(); + } + + public static PemCertificate Instance { get; } + = new PemCertificate("localhost"); + + public string CommonName { get; } + + public string Thumbprint { get; } + + public string Certificate { get; } + + public string CertificateKey { get; } +} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantDefaultContainerTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantDefaultContainerTest.cs new file mode 100644 index 000000000..eef79f1e8 --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/QdrantDefaultContainerTest.cs @@ -0,0 +1,49 @@ +namespace Testcontainers.Qdrant; + +public sealed class QdrantDefaultContainerTest : IAsyncLifetime +{ + // # --8<-- [start:UseQdrantContainer] + private readonly QdrantContainer _qdrantContainer = new QdrantBuilder().Build(); + + public Task InitializeAsync() + { + return _qdrantContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _qdrantContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task HealthReturnsValidResponse() + { + // Given + using var client = new QdrantClient(new Uri(_qdrantContainer.GetGrpcConnectionString())); + + // When + var response = await client.HealthAsync() + .ConfigureAwait(true); + + // Then + Assert.NotEmpty(response.Title); + } + // # --8<-- [end:UseQdrantContainer] + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task GetRootEndpointReturnsHttpStatusCodeOk() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()); + + // When + using var httpResponse = await httpClient.GetAsync("/") + .ConfigureAwait(true); + + // Then + Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs new file mode 100644 index 000000000..64d84b6cb --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs @@ -0,0 +1,116 @@ +namespace Testcontainers.Qdrant; + +public sealed class QdrantSecureContainerTest : IAsyncLifetime +{ + private static readonly string ApiKey = Guid.NewGuid().ToString("D"); + + private static readonly string CommonName = PemCertificate.Instance.CommonName; + + private static readonly string Certificate = PemCertificate.Instance.Certificate; + + private static readonly string CertificateKey = PemCertificate.Instance.CertificateKey; + + private static readonly string Thumbprint = PemCertificate.Instance.Thumbprint; + + private readonly QdrantContainer _qdrantContainer = new QdrantBuilder() + // # --8<-- [start:ConfigureQdrantContainerApiKey] + .WithApiKey(ApiKey) + // # --8<-- [end:ConfigureQdrantContainerApiKey] + + // # --8<-- [start:ConfigureQdrantContainerCertificate] + .WithCertificate(Certificate, CertificateKey) + // # --8<-- [end:ConfigureQdrantContainerCertificate] + .Build(); + + public Task InitializeAsync() + { + return _qdrantContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _qdrantContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task HealthReturnsValidResponse() + { + // Given + // # --8<-- [start:ConfigureQdrantClientCertificate-1] + using var httpMessageHandler = new HttpClientHandler(); + httpMessageHandler.ServerCertificateCustomValidationCallback = CertificateValidation.Thumbprint(Thumbprint); + + using var httpClient = new HttpClient(httpMessageHandler); + httpClient.DefaultRequestHeaders.Host = CommonName; + // # --8<-- [end:ConfigureQdrantClientCertificate-1] + + // # --8<-- [start:ConfigureQdrantClientApiKey] + httpClient.DefaultRequestHeaders.Add("api-key", ApiKey); + // # --8<-- [end:ConfigureQdrantClientApiKey] + + // # --8<-- [start:ConfigureQdrantClientCertificate-2] + var grpcChannelOptions = new GrpcChannelOptions(); + grpcChannelOptions.HttpClient = httpClient; + + using var grpcChannel = GrpcChannel.ForAddress(_qdrantContainer.GetGrpcConnectionString(), grpcChannelOptions); + + using var grpcClient = new QdrantGrpcClient(grpcChannel); + + using var client = new QdrantClient(grpcClient); + // # --8<-- [end:ConfigureQdrantClientCertificate-2] + + // When + var response = await client.HealthAsync() + .ConfigureAwait(true); + + // Then + Assert.NotEmpty(response.Title); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task HealthWithoutApiKeyReturnsUnauthenticated() + { + // Given + using var httpMessageHandler = new HttpClientHandler(); + httpMessageHandler.ServerCertificateCustomValidationCallback = CertificateValidation.Thumbprint(Thumbprint); + + using var httpClient = new HttpClient(httpMessageHandler); + httpClient.DefaultRequestHeaders.Host = CommonName; + + var grpcChannelOptions = new GrpcChannelOptions(); + grpcChannelOptions.HttpClient = httpClient; + + using var grpcChannel = GrpcChannel.ForAddress(_qdrantContainer.GetGrpcConnectionString(), grpcChannelOptions); + + using var grpcClient = new QdrantGrpcClient(grpcChannel); + + using var client = new QdrantClient(grpcClient); + + // When + var exception = await Assert.ThrowsAsync(() => client.HealthAsync()) + .ConfigureAwait(true); + + // Then + Assert.Equal(StatusCode.Unauthenticated, exception.Status.StatusCode); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task HealthWithoutCertificateValidationReturnsSecureConnectionError() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()); + httpClient.DefaultRequestHeaders.Host = CommonName; + httpClient.DefaultRequestHeaders.Add("api-key", ApiKey); + + // When + var exception = await Assert.ThrowsAsync(() => httpClient.GetAsync("/")) + .ConfigureAwait(true); + + // Then + Assert.Equal(HttpRequestError.SecureConnectionError, exception.HttpRequestError); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj new file mode 100644 index 000000000..73a730e6d --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj @@ -0,0 +1,20 @@ + + + net9.0 + false + false + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/Usings.cs b/tests/Testcontainers.Qdrant.Tests/Usings.cs new file mode 100644 index 000000000..4923c28cb --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/Usings.cs @@ -0,0 +1,13 @@ +global using System; +global using System.Net; +global using System.Net.Http; +global using System.Security.Cryptography; +global using System.Security.Cryptography.X509Certificates; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using Grpc.Core; +global using Grpc.Core.Interceptors; +global using Grpc.Net.Client; +global using Qdrant.Client; +global using Qdrant.Client.Grpc; +global using Xunit; \ No newline at end of file diff --git a/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs b/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs index 5944fae3f..ea9c9e046 100644 --- a/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs +++ b/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs @@ -9,6 +9,7 @@ private ServiceBusContainerTest(ServiceBusContainer serviceBusContainer) _serviceBusContainer = serviceBusContainer; } + // # --8<-- [start:UseServiceBusContainer] public Task InitializeAsync() { return _serviceBusContainer.StartAsync(); @@ -52,7 +53,9 @@ await sender.SendMessageAsync(message) // Then Assert.Equal(helloServiceBus, receivedMessage.Body.ToString()); } + // # --8<-- [end:UseServiceBusContainer] + // # --8<-- [start:CreateServiceBusContainer] [UsedImplicitly] public sealed class ServiceBusDefaultMsSqlConfiguration : ServiceBusContainerTest { @@ -63,6 +66,7 @@ public ServiceBusDefaultMsSqlConfiguration() { } } + // # --8<-- [end:CreateServiceBusContainer] [UsedImplicitly] public sealed class ServiceBusCustomMsSqlConfiguration : ServiceBusContainerTest, IClassFixture @@ -70,7 +74,9 @@ public sealed class ServiceBusCustomMsSqlConfiguration : ServiceBusContainerTest public ServiceBusCustomMsSqlConfiguration(DatabaseFixture fixture) : base(new ServiceBusBuilder() .WithAcceptLicenseAgreement(true) + // # --8<-- [start:ReuseExistingMsSqlContainer] .WithMsSqlContainer(fixture.Network, fixture.Container, DatabaseFixture.DatabaseNetworkAlias) + // # --8<-- [end:ReuseExistingMsSqlContainer] .Build()) { } diff --git a/tests/Testcontainers.ServiceBus.Tests/Testcontainers.ServiceBus.Tests.csproj b/tests/Testcontainers.ServiceBus.Tests/Testcontainers.ServiceBus.Tests.csproj index 835a69422..6c81fbd03 100644 --- a/tests/Testcontainers.ServiceBus.Tests/Testcontainers.ServiceBus.Tests.csproj +++ b/tests/Testcontainers.ServiceBus.Tests/Testcontainers.ServiceBus.Tests.csproj @@ -5,11 +5,13 @@ false + + diff --git a/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs b/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs index fa472ed8e..ecbe234b9 100644 --- a/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs +++ b/tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs @@ -11,7 +11,7 @@ public abstract class WebDriverContainerTest : IAsyncLifetime private WebDriverContainerTest(WebDriverContainer webDriverContainer) { _helloWorldContainer = new ContainerBuilder() - .WithImage("testcontainers/helloworld:1.1.0") + .WithImage(CommonImages.HelloWorld) .WithNetwork(webDriverContainer.GetNetwork()) .WithNetworkAliases(_helloWorldBaseAddress.Host) .WithPortBinding(_helloWorldBaseAddress.Port, true)