diff --git a/Testcontainers.slnx b/Testcontainers.slnx
index 6b046c1f1..3dfc43113 100644
--- a/Testcontainers.slnx
+++ b/Testcontainers.slnx
@@ -37,6 +37,7 @@
+
@@ -108,6 +109,7 @@
+
diff --git a/docs/modules/flociaz.md b/docs/modules/flociaz.md
new file mode 100644
index 000000000..4b965713e
--- /dev/null
+++ b/docs/modules/flociaz.md
@@ -0,0 +1,90 @@
+# FlociAz
+
+[FlociAz](https://github.com/floci-io/floci-az) emulates Azure management and data-plane APIs in one container. The module starts in a Docker-safe mode: services that would otherwise create child containers use their mocked or embedded implementations.
+
+Add the module to a test project:
+
+```shell
+dotnet add package Testcontainers.FlociAz
+```
+
+Start FlociAz and use its storage connection string or service-specific endpoints:
+
+```csharp
+await using var flociAz = new FlociAzBuilder("floci/floci-az:0.12.0")
+ .Build();
+
+await flociAz.StartAsync();
+
+var blobs = new BlobServiceClient(flociAz.GetConnectionString());
+var keyVaultEndpoint = flociAz.GetServiceEndpoint("keyvault");
+var armEndpoint = flociAz.GetEndpoint();
+```
+
+## Service compatibility
+
+The following matrix is covered against FlociAz 0.12.0. “Real” means the test reaches the service's actual protocol or runtime, not only its ARM representation.
+
+| Service | Verified compatibility |
+|---------|------------------------|
+| Blob Storage | Azure Storage SDK create, upload, and download |
+| Queue Storage | Azure Storage SDK create, send, and receive |
+| Table Storage | Azure Data Tables SDK create, insert, and read |
+| Functions | Management lifecycle, mocked invocation, and real Node.js runtime execution |
+| App Configuration | Key-value write and read |
+| Cosmos DB for NoSQL | Database, container, and partitioned document lifecycle |
+| Key Vault | Authenticated secret write and read |
+| Event Hubs | Mocked namespace management only |
+| Azure SQL Database | ARM server lifecycle in the default management-only provider |
+| Azure Database for PostgreSQL | ARM lifecycle and real Npgsql query |
+| Service Bus | Mocked queue/topic/subscription/rule topology and real Azure SDK AMQP send/receive |
+| Azure Monitor | Workspace, collection endpoint/rule, log ingestion, and KQL query |
+| AKS | Mocked ARM cluster lifecycle |
+| Azure Container Instances | Mocked ARM container-group lifecycle |
+| Virtual Machines | Mocked ARM VM lifecycle |
+| API Management | ARM service lifecycle |
+| Azure Cache for Redis | ARM lifecycle and real RESP write/read |
+| Azure Container Registry | ARM lifecycle and real Registry V2 API |
+| Microsoft Entra ID | OAuth client-credentials token issuance |
+| Microsoft Graph | Service-principal discovery and seeded group membership |
+| Communication Services Email | Send operation and inspection mailbox |
+| Azure Resource Manager | Resource-group and service resource lifecycle |
+| Virtual Network | ARM virtual-network lifecycle |
+| Event Grid | Topic keys, event publication, and lifecycle |
+| Managed Identity | User-assigned ARM lifecycle and IMDS token issuance |
+
+### Upstream 0.12.0 boundaries
+
+- Event Hubs AMQP is deliberately hard-coded to mocked mode upstream because Azure SDK connections reset.
+- Azure Container Instances accepts `mocked=false`, but 0.12.0 still behaves as mocked mode; container-backed mode is planned upstream.
+- AKS real mode starts k3s, but does not reliably transition the ARM resource from `Creating` to `Succeeded` in the containerized Testcontainers topology. The module therefore defaults it to mocked mode.
+- Azure SQL's managed data plane requires explicit acceptance of the Microsoft SQL Server EULA. The module never accepts it on the user's behalf; enable and test that mode only after reviewing the license.
+
+## Docker-backed services
+
+Functions, PostgreSQL, Service Bus, Redis, and ACR have verified real modes. Grant FlociAz Docker access and opt individual services into real mode:
+
+```csharp
+await using var flociAz = new FlociAzBuilder("floci/floci-az:0.12.0")
+ .WithDockerSocket()
+ .WithEnvironment("FLOCI_AZ_SERVICES_FUNCTIONS_MOCKED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_POSTGRES_MOCKED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_REDIS_MOCKED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_ACR_MOCKED", "false")
+ .Build();
+```
+
+!!! warning
+
+ The Docker socket provides root-equivalent access to the Docker host. Use `WithDockerSocket()` only with trusted images. Child containers and volumes are namespaced and registered with the Testcontainers Resource Reaper.
+
+FlociAz `/connect` responses contain the child container's internal hostname and port. Resolve that pair to a host port before connecting from the test process:
+
+```csharp
+var mappedPort = await flociAz.GetSidecarMappedPublicPortAsync(
+ sidecarHostname,
+ sidecarPrivatePort);
+```
+
+Use `flociAz.Hostname` with the returned port. This works with local and remote Docker endpoints supported by Testcontainers.
diff --git a/docs/modules/index.md b/docs/modules/index.md
index dfb43b2a2..6165f07a2 100644
--- a/docs/modules/index.md
+++ b/docs/modules/index.md
@@ -44,6 +44,7 @@ await moduleNameContainer.StartAsync();
| Firebird | `jacobalberty/firebird:v4.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.FirebirdSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.FirebirdSql) |
| Firestore | `gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators` | [NuGet](https://www.nuget.org/packages/Testcontainers.Firestore) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Firestore) |
| Floci | `floci/floci:1.5.13` | [NuGet](https://www.nuget.org/packages/Testcontainers.Floci) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Floci) |
+| FlociAz | `floci/floci-az:0.12.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.FlociAz) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.FlociAz) |
| Grafana | `grafana/grafana:12.2` | [NuGet](https://www.nuget.org/packages/Testcontainers.Grafana) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Grafana) |
| InfluxDB | `influxdb:2.7` | [NuGet](https://www.nuget.org/packages/Testcontainers.InfluxDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.InfluxDb) |
| JanusGraph | `janusgraph/janusgraph:1.0.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.JanusGraph) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.JanusGraph) |
diff --git a/mkdocs.yml b/mkdocs.yml
index b4f0a73cb..46d034c47 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -59,6 +59,7 @@ nav:
- modules/pulsar.md # Apache
- modules/aspire-dashboard.md
- modules/eventhubs.md # Azure
+ - modules/flociaz.md # Azure
- modules/servicebus.md # Azure
- modules/clickhouse.md
- modules/db2.md
@@ -78,4 +79,4 @@ nav:
- modules/toxiproxy.md
- modules/valkey.md
- contributing.md
- - contributing_docs.md
\ No newline at end of file
+ - contributing_docs.md
diff --git a/src/Testcontainers.FlociAz/.editorconfig b/src/Testcontainers.FlociAz/.editorconfig
new file mode 100644
index 000000000..78b36ca08
--- /dev/null
+++ b/src/Testcontainers.FlociAz/.editorconfig
@@ -0,0 +1 @@
+root = true
diff --git a/src/Testcontainers.FlociAz/FlociAzBuilder.cs b/src/Testcontainers.FlociAz/FlociAzBuilder.cs
new file mode 100644
index 000000000..2c97c5ede
--- /dev/null
+++ b/src/Testcontainers.FlociAz/FlociAzBuilder.cs
@@ -0,0 +1,122 @@
+namespace Testcontainers.FlociAz;
+
+///
+[PublicAPI]
+public sealed class FlociAzBuilder : ContainerBuilder
+{
+ private const string DockerSocket = "/var/run/docker.sock";
+
+ public const ushort FlociAzPort = 4577;
+
+ public const string AccountName = "devstoreaccount1";
+
+ public const string AccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The full Docker image name, including the image repository and tag
+ /// (e.g., floci/floci-az:0.12.0).
+ ///
+ ///
+ /// Docker image tags available at .
+ ///
+ public FlociAzBuilder(string image)
+ : this(new DockerImage(image))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// An instance that specifies the Docker image to be used
+ /// for the container builder configuration.
+ ///
+ ///
+ /// Docker image tags available at .
+ ///
+ public FlociAzBuilder(IImage image)
+ : this(new FlociAzConfiguration())
+ {
+ DockerResourceConfiguration = Init().WithImage(image).DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private FlociAzBuilder(FlociAzConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override FlociAzConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Grants FlociAz access to the Docker daemon for services that use sidecar containers.
+ ///
+ ///
+ /// The Docker socket provides root-equivalent access to the Docker host. Only enable it for
+ /// trusted images. FlociAz child containers and volumes receive a unique namespace that is
+ /// registered with the Testcontainers Resource Reaper.
+ ///
+ /// The host Docker socket path, or null to detect it.
+ /// A configured instance of .
+ public FlociAzBuilder WithDockerSocket(string dockerSocket = null)
+ {
+ var endpoint = DockerResourceConfiguration.DockerEndpointAuthConfig.Endpoint;
+ var detectedSocket = endpoint.Scheme.Equals("unix", StringComparison.OrdinalIgnoreCase) ? endpoint.AbsolutePath : DockerSocket;
+ var source = dockerSocket ?? TestcontainersSettings.DockerSocketOverride ?? detectedSocket;
+ var resourceNamespace = "tc-" + Guid.NewGuid().ToString("N");
+
+ return WithBindMount(source, DockerSocket, AccessMode.ReadWrite)
+ .WithEnvironment("FLOCI_AZ_DOCKER_RESOURCE_NAMESPACE", resourceNamespace);
+ }
+
+ ///
+ public override FlociAzContainer Build()
+ {
+ Validate();
+ return new FlociAzContainer(DockerResourceConfiguration);
+ }
+
+ ///
+ protected override FlociAzBuilder Init()
+ {
+ return base.Init()
+ .WithPortBinding(FlociAzPort, true)
+ .WithEnvironment("FLOCI_AZ_SERVICES_EVENT_HUB_ENABLED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_FUNCTIONS_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_POSTGRES_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_AKS_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_ACR_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_REDIS_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_COSMOS_MOCKED", "true")
+ .WithConnectionStringProvider(new FlociAzConnectionStringProvider())
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
+ request.ForPath("/_floci/health").ForPort(FlociAzPort)));
+ }
+
+ ///
+ protected override FlociAzBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new FlociAzConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override FlociAzBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new FlociAzConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override FlociAzBuilder Merge(FlociAzConfiguration oldValue, FlociAzConfiguration newValue)
+ {
+ return new FlociAzBuilder(new FlociAzConfiguration(oldValue, newValue));
+ }
+}
diff --git a/src/Testcontainers.FlociAz/FlociAzConfiguration.cs b/src/Testcontainers.FlociAz/FlociAzConfiguration.cs
new file mode 100644
index 000000000..b24d5f5dc
--- /dev/null
+++ b/src/Testcontainers.FlociAz/FlociAzConfiguration.cs
@@ -0,0 +1,41 @@
+namespace Testcontainers.FlociAz;
+
+///
+[PublicAPI]
+public sealed class FlociAzConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FlociAzConfiguration()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public FlociAzConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public FlociAzConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old FlociAz configuration.
+ /// The new FlociAz configuration.
+ public FlociAzConfiguration(FlociAzConfiguration oldValue, FlociAzConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ }
+}
diff --git a/src/Testcontainers.FlociAz/FlociAzConnectionStringProvider.cs b/src/Testcontainers.FlociAz/FlociAzConnectionStringProvider.cs
new file mode 100644
index 000000000..95c2881fc
--- /dev/null
+++ b/src/Testcontainers.FlociAz/FlociAzConnectionStringProvider.cs
@@ -0,0 +1,13 @@
+namespace Testcontainers.FlociAz;
+
+///
+/// Provides the FlociAz connection string.
+///
+internal sealed class FlociAzConnectionStringProvider : ContainerConnectionStringProvider
+{
+ ///
+ protected override string GetHostConnectionString()
+ {
+ return Container.GetConnectionString();
+ }
+}
diff --git a/src/Testcontainers.FlociAz/FlociAzContainer.cs b/src/Testcontainers.FlociAz/FlociAzContainer.cs
new file mode 100644
index 000000000..b0cc5e497
--- /dev/null
+++ b/src/Testcontainers.FlociAz/FlociAzContainer.cs
@@ -0,0 +1,127 @@
+namespace Testcontainers.FlociAz;
+
+///
+[PublicAPI]
+public sealed class FlociAzContainer : DockerContainer
+{
+ private const string ResourceNamespaceEnvironmentVariable = "FLOCI_AZ_DOCKER_RESOURCE_NAMESPACE";
+
+ private readonly FlociAzConfiguration _configuration;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public FlociAzContainer(FlociAzConfiguration configuration)
+ : base(configuration)
+ {
+ _configuration = configuration;
+ }
+
+ ///
+ public override async Task StartAsync(CancellationToken ct = default)
+ {
+ await base.StartAsync(ct)
+ .ConfigureAwait(false);
+
+ if (Guid.Empty.Equals(_configuration.SessionId)
+ || !_configuration.Environments.TryGetValue(ResourceNamespaceEnvironmentVariable, out var resourceNamespace)
+ || string.IsNullOrEmpty(resourceNamespace))
+ {
+ return;
+ }
+
+ var resourceReaper = await ResourceReaper.GetAndStartDefaultAsync(_configuration.DockerEndpointAuthConfig, _configuration.Logger, ct: ct)
+ .ConfigureAwait(false);
+ await resourceReaper.RegisterFilterAsync($"label=floci_namespace={resourceNamespace}", ct)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets the FlociAz storage connection string.
+ ///
+ /// The FlociAz storage connection string.
+ public string GetConnectionString()
+ {
+ var properties = new Dictionary();
+ properties.Add("DefaultEndpointsProtocol", Uri.UriSchemeHttp);
+ properties.Add("AccountName", FlociAzBuilder.AccountName);
+ properties.Add("AccountKey", FlociAzBuilder.AccountKey);
+ properties.Add("BlobEndpoint", GetServiceEndpoint());
+ properties.Add("QueueEndpoint", GetServiceEndpoint("queue"));
+ properties.Add("TableEndpoint", GetServiceEndpoint("table"));
+ return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value)));
+ }
+
+ ///
+ /// Gets the FlociAz endpoint used by root-level and Azure Resource Manager APIs.
+ ///
+ /// The FlociAz endpoint.
+ public string GetEndpoint()
+ {
+ return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(FlociAzBuilder.FlociAzPort)).ToString();
+ }
+
+ ///
+ /// Gets the FlociAz service endpoint.
+ ///
+ ///
+ /// FlociAz routes its REST services by path: the blob storage service uses the
+ /// bare /{accountName} path (omit the argument),
+ /// other services use /{accountName}-{service} (e.g., queue,
+ /// table, cosmos, keyvault, appconfig, functions).
+ ///
+ /// The service name, or null for the blob storage endpoint.
+ /// The FlociAz service endpoint.
+ public string GetServiceEndpoint(string service = null)
+ {
+ var path = string.IsNullOrEmpty(service) ? FlociAzBuilder.AccountName : string.Join("-", FlociAzBuilder.AccountName, service);
+ return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(FlociAzBuilder.FlociAzPort), path + "/").ToString();
+ }
+
+ ///
+ /// Gets the host port mapped to a FlociAz sidecar container port.
+ ///
+ /// The sidecar hostname returned by FlociAz.
+ /// The sidecar container port.
+ /// The cancellation token.
+ /// The Docker host port mapped to .
+ ///
+ /// The container was not configured with , or
+ /// the requested sidecar or port does not exist.
+ ///
+ public async Task GetSidecarMappedPublicPortAsync(string sidecarHostname, ushort privatePort, CancellationToken ct = default)
+ {
+ if (!_configuration.Environments.TryGetValue(ResourceNamespaceEnvironmentVariable, out var resourceNamespace)
+ || string.IsNullOrEmpty(resourceNamespace))
+ {
+ throw new InvalidOperationException($"Configure the container with {nameof(FlociAzBuilder)}.{nameof(FlociAzBuilder.WithDockerSocket)}() before resolving sidecar ports.");
+ }
+
+ using var dockerClient = _configuration.DockerEndpointAuthConfig.GetDockerClientBuilder(_configuration.SessionId).Build();
+ var filters = new Dictionary>
+ {
+ ["label"] = new Dictionary { [$"floci_namespace={resourceNamespace}"] = true },
+ };
+ var sidecars = await dockerClient.Containers.ListContainersAsync(new ContainersListParameters { All = true, Filters = filters }, ct)
+ .ConfigureAwait(false);
+ var sidecar = sidecars.SingleOrDefault(container => container.Names.Any(name => sidecarHostname.Equals(name.TrimStart('/'), StringComparison.Ordinal)));
+ var port = sidecar?.Ports.FirstOrDefault(binding => binding.PrivatePort == privatePort && binding.PublicPort > 0);
+
+ return port == null
+ ? throw new InvalidOperationException($"FlociAz sidecar '{sidecarHostname}' does not expose container port {privatePort}.")
+ : checked((ushort)port.PublicPort);
+ }
+
+ ///
+ /// Gets the FlociAz Cosmos DB connection string.
+ ///
+ /// The FlociAz Cosmos DB connection string.
+ public string GetCosmosConnectionString()
+ {
+ var properties = new Dictionary();
+ properties.Add("AccountEndpoint", GetServiceEndpoint("cosmos"));
+ properties.Add("AccountKey", FlociAzBuilder.AccountKey);
+ return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value)));
+ }
+}
diff --git a/src/Testcontainers.FlociAz/Testcontainers.FlociAz.csproj b/src/Testcontainers.FlociAz/Testcontainers.FlociAz.csproj
new file mode 100644
index 000000000..16967e2c6
--- /dev/null
+++ b/src/Testcontainers.FlociAz/Testcontainers.FlociAz.csproj
@@ -0,0 +1,12 @@
+
+
+ net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
diff --git a/src/Testcontainers.FlociAz/Usings.cs b/src/Testcontainers.FlociAz/Usings.cs
new file mode 100644
index 000000000..b39130814
--- /dev/null
+++ b/src/Testcontainers.FlociAz/Usings.cs
@@ -0,0 +1,11 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Linq;
+global using System.Threading;
+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 DotNet.Testcontainers.Images;
+global using JetBrains.Annotations;
diff --git a/tests/Testcontainers.FlociAz.Tests/.editorconfig b/tests/Testcontainers.FlociAz.Tests/.editorconfig
new file mode 100644
index 000000000..78b36ca08
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
diff --git a/tests/Testcontainers.FlociAz.Tests/.runs-on b/tests/Testcontainers.FlociAz.Tests/.runs-on
new file mode 100644
index 000000000..2c0c1ac72
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/.runs-on
@@ -0,0 +1 @@
+ubuntu-24.04
diff --git a/tests/Testcontainers.FlociAz.Tests/Dockerfile b/tests/Testcontainers.FlociAz.Tests/Dockerfile
new file mode 100644
index 000000000..7e2806e99
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/Dockerfile
@@ -0,0 +1 @@
+FROM floci/floci-az:0.12.0@sha256:0c673d49bb75b502ea0750f1c1347777483ffc33945539e1d9254438cb441a03
diff --git a/tests/Testcontainers.FlociAz.Tests/FlociAzContainerTest.Compatibility.cs b/tests/Testcontainers.FlociAz.Tests/FlociAzContainerTest.Compatibility.cs
new file mode 100644
index 000000000..573c6c58d
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/FlociAzContainerTest.Compatibility.cs
@@ -0,0 +1,251 @@
+namespace Testcontainers.FlociAz;
+
+public sealed partial class FlociAzContainerTest
+{
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "eventhub")]
+ public async Task MockedEventHubSupportsNamespaceManagement()
+ {
+ // Given
+ var namespaceName = "eventhub-" + Guid.NewGuid().ToString("N");
+ using var client = fixture.CreateHttpClient("eventhub");
+
+ // When
+ using var createResponse = await client.PutAsJsonAsync($"namespaces/{namespaceName}", new { entities = "events:2", consumerGroups = "$Default,test" }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(createResponse).ConfigureAwait(true);
+
+ using var listResponse = await client.GetAsync("namespaces", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var namespaces = await ReadJsonAsync(listResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Contains(namespaces.RootElement.GetProperty("namespaces").EnumerateArray(), item => item.GetProperty("name").GetString() == namespaceName);
+
+ using var deleteResponse = await client.DeleteAsync($"namespaces/{namespaceName}", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(deleteResponse).ConfigureAwait(true);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "graph")]
+ public async Task GraphReturnsSeededDirectoryMembership()
+ {
+ // Given
+ using var client = fixture.CreateHttpClient();
+
+ // When
+ using var groupsResponse = await client.PostAsJsonAsync("v1.0/users/dev-user@floci-az.local/getMemberGroups", new { securityEnabledOnly = true }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var groups = await ReadJsonAsync(groupsResponse).ConfigureAwait(true);
+
+ using var principalsResponse = await client.GetAsync("v1.0/servicePrincipals?$filter=appId%20eq%20'11111111-1111-1111-1111-111111111111'", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var principals = await ReadJsonAsync(principalsResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Contains(groups.RootElement.GetProperty("value").EnumerateArray(), group => group.GetString() == "44444444-4444-4444-4444-444444444444");
+ Assert.NotEmpty(principals.RootElement.GetProperty("value").EnumerateArray());
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "functions")]
+ public async Task MockedFunctionSupportsDeploymentInvocationAndDeletion()
+ {
+ // Given
+ var appName = "app-" + Guid.NewGuid().ToString("N");
+ var functionName = "function-" + Guid.NewGuid().ToString("N");
+ using var client = fixture.CreateHttpClient("functions");
+ using var package = CreateFunctionPackage(functionName);
+
+ // When
+ using var appResponse = await client.PutAsJsonAsync($"admin/apps/{appName}", new { runtime = "node" }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(appResponse).ConfigureAwait(true);
+
+ using var deployResponse = await client.PutAsJsonAsync($"admin/apps/{appName}/functions/{functionName}", new { handler = "index.handler", zipBase64 = Convert.ToBase64String(package.ToArray()) }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(deployResponse).ConfigureAwait(true);
+
+ using var listResponse = await client.GetAsync($"admin/apps/{appName}/functions", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var functions = await ReadJsonAsync(listResponse).ConfigureAwait(true);
+
+ using var invocationResponse = await client.PostAsJsonAsync($"api/{appName}/{functionName}", new { value = "test" }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(invocationResponse).ConfigureAwait(true);
+
+ using var deleteResponse = await client.DeleteAsync($"admin/apps/{appName}/functions/{functionName}", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(deleteResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Contains(functions.RootElement.GetProperty("value").EnumerateArray(), function => function.GetProperty("name").GetString() == functionName);
+ using var deletedResponse = await client.GetAsync($"admin/apps/{appName}/functions/{functionName}", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ Assert.Equal(HttpStatusCode.NotFound, deletedResponse.StatusCode);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "servicebus")]
+ public async Task MockedServiceBusSupportsQueueTopicSubscriptionAndRuleTopology()
+ {
+ // Given
+ var queueName = "queue-" + Guid.NewGuid().ToString("N");
+ var topicName = "topic-" + Guid.NewGuid().ToString("N");
+ var subscriptionName = "subscription-" + Guid.NewGuid().ToString("N");
+ var ruleName = "rule-" + Guid.NewGuid().ToString("N");
+ using var client = fixture.CreateHttpClient("servicebus");
+
+ // When
+ using var queueResponse = await PutXmlAsync(client, queueName, "").ConfigureAwait(true);
+ await AssertSuccessAsync(queueResponse).ConfigureAwait(true);
+
+ using var topicResponse = await PutXmlAsync(client, topicName, "").ConfigureAwait(true);
+ await AssertSuccessAsync(topicResponse).ConfigureAwait(true);
+
+ using var subscriptionResponse = await PutXmlAsync(client, $"{topicName}/subscriptions/{subscriptionName}", "").ConfigureAwait(true);
+ await AssertSuccessAsync(subscriptionResponse).ConfigureAwait(true);
+
+ const string rule = "";
+ using var ruleResponse = await PutXmlAsync(client, $"{topicName}/subscriptions/{subscriptionName}/rules/{ruleName}", rule).ConfigureAwait(true);
+ await AssertSuccessAsync(ruleResponse).ConfigureAwait(true);
+
+ using var queuesResponse = await client.GetAsync("$Resources/queues", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(queuesResponse).ConfigureAwait(true);
+ var queues = await queuesResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ using var rulesResponse = await client.GetAsync($"{topicName}/subscriptions/{subscriptionName}/rules", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(rulesResponse).ConfigureAwait(true);
+ var rules = await rulesResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ // Then
+ Assert.Contains(queueName, queues, StringComparison.Ordinal);
+ Assert.Contains(ruleName, rules, StringComparison.Ordinal);
+
+ using var deleteRuleResponse = await client.DeleteAsync($"{topicName}/subscriptions/{subscriptionName}/rules/{ruleName}", TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteRuleResponse).ConfigureAwait(true);
+ using var deleteSubscriptionResponse = await client.DeleteAsync($"{topicName}/subscriptions/{subscriptionName}", TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteSubscriptionResponse).ConfigureAwait(true);
+ using var deleteTopicResponse = await client.DeleteAsync(topicName, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteTopicResponse).ConfigureAwait(true);
+ using var deleteQueueResponse = await client.DeleteAsync(queueName, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteQueueResponse).ConfigureAwait(true);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "monitor")]
+ public async Task MonitorIngestsAndQueriesCustomLogs()
+ {
+ // Given
+ var workspaceName = "ws" + Guid.NewGuid().ToString("N");
+ var endpointName = "dce" + Guid.NewGuid().ToString("N");
+ var ruleName = "dcr" + Guid.NewGuid().ToString("N");
+ var providers = $"subscriptions/{FlociAzFixture.SubscriptionId}/resourceGroups/{FlociAzFixture.ResourceGroup}/providers";
+ var workspacePath = $"{providers}/Microsoft.OperationalInsights/workspaces/{workspaceName}?api-version=2023-09-01";
+ var endpointPath = $"{providers}/Microsoft.Insights/dataCollectionEndpoints/{endpointName}?api-version=2023-09-01";
+ var rulePath = $"{providers}/Microsoft.Insights/dataCollectionRules/{ruleName}?api-version=2023-09-01";
+ var workspaceId = $"/{providers}/Microsoft.OperationalInsights/workspaces/{workspaceName}";
+ using var client = fixture.CreateHttpClient();
+
+ // When
+ using var workspaceResponse = await client.PutAsJsonAsync(workspacePath, new { location = "eastus", properties = new { } }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ using var workspace = await ReadJsonAsync(workspaceResponse).ConfigureAwait(true);
+ var customerId = workspace.RootElement.GetProperty("properties").GetProperty("customerId").GetString();
+
+ using var endpointResponse = await client.PutAsJsonAsync(endpointPath, new { location = "eastus", properties = new { } }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(endpointResponse).ConfigureAwait(true);
+
+ var rulePayload = new
+ {
+ location = "eastus",
+ properties = new
+ {
+ destinations = new { logAnalytics = new[] { new { name = "workspace", workspaceResourceId = workspaceId } } },
+ },
+ };
+ using var ruleResponse = await client.PutAsJsonAsync(rulePath, rulePayload, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ using var ruleDocument = await ReadJsonAsync(ruleResponse).ConfigureAwait(true);
+ var immutableId = ruleDocument.RootElement.GetProperty("properties").GetProperty("immutableId").GetString();
+
+ var records = new[] { new { TimeGenerated = "2026-09-01T12:00:00Z", Level = "ERROR", Message = "compatible" } };
+ using var ingestResponse = await client.PostAsJsonAsync($"dataCollectionRules/{immutableId}/streams/Custom-Test_CL?api-version=2023-01-01", records, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(ingestResponse).ConfigureAwait(true);
+
+ using var queryResponse = await client.PostAsJsonAsync($"v1/workspaces/{customerId}/query", new { query = "Test_CL | where level == 'ERROR' | project message | take 1" }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ using var query = await ReadJsonAsync(queryResponse).ConfigureAwait(true);
+
+ // Then
+ var table = Assert.Single(query.RootElement.GetProperty("tables").EnumerateArray());
+ var row = Assert.Single(table.GetProperty("rows").EnumerateArray());
+ Assert.Equal("compatible", row[0].GetString());
+
+ using var deleteRuleResponse = await client.DeleteAsync(rulePath, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteRuleResponse).ConfigureAwait(true);
+ using var deleteEndpointResponse = await client.DeleteAsync(endpointPath, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteEndpointResponse).ConfigureAwait(true);
+ using var deleteWorkspaceResponse = await client.DeleteAsync(workspacePath, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteWorkspaceResponse).ConfigureAwait(true);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "eventgrid")]
+ public async Task EventGridReturnsKeysAndAcceptsPublishedEvents()
+ {
+ // Given
+ var topicName = "topic" + Guid.NewGuid().ToString("N");
+ var topicPath = $"subscriptions/{FlociAzFixture.SubscriptionId}/resourceGroups/{FlociAzFixture.ResourceGroup}/providers/Microsoft.EventGrid/topics/{topicName}?api-version=2025-02-15";
+ using var client = fixture.CreateHttpClient();
+
+ // When
+ using var topicResponse = await client.PutAsJsonAsync(topicPath, new { location = "eastus", properties = new { inputSchema = "EventGridSchema" } }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(topicResponse).ConfigureAwait(true);
+
+ using var keysResponse = await client.PostAsJsonAsync(topicPath.Replace("?", "/listKeys?", StringComparison.Ordinal), new { }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ using var keys = await ReadJsonAsync(keysResponse).ConfigureAwait(true);
+
+ client.DefaultRequestHeaders.Add("aeg-sas-key", keys.RootElement.GetProperty("key1").GetString());
+ var events = new[] { new { id = Guid.NewGuid().ToString("D"), subject = "/test/1", eventType = "Test.Created", eventTime = DateTimeOffset.UtcNow, data = new { value = 1 }, dataVersion = "1.0" } };
+ using var publishResponse = await client.PostAsJsonAsync($"{topicName}-eventgrid/api/events", events, TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ // Then
+ await AssertSuccessAsync(publishResponse).ConfigureAwait(true);
+
+ using var deleteResponse = await client.DeleteAsync(topicPath, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteResponse).ConfigureAwait(true);
+ }
+
+ private static async Task PutXmlAsync(HttpClient client, string path, string xml)
+ {
+ return await client.PutAsync(path, new StringContent(xml, Encoding.UTF8, "application/atom+xml"), TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ }
+
+ private static MemoryStream CreateFunctionPackage(string functionName)
+ {
+ var stream = new MemoryStream();
+ using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
+ {
+ WriteEntry(archive, "host.json", "{\"version\":\"2.0\"}");
+ WriteEntry(archive, $"{functionName}/function.json", "{\"bindings\":[{\"authLevel\":\"anonymous\",\"type\":\"httpTrigger\",\"direction\":\"in\",\"name\":\"req\"},{\"type\":\"http\",\"direction\":\"out\",\"name\":\"res\"}]}");
+ WriteEntry(archive, $"{functionName}/index.js", "module.exports = async () => ({ status: 200, body: 'compatible' });");
+ }
+
+ stream.Position = 0;
+ return stream;
+ }
+
+ private static void WriteEntry(ZipArchive archive, string path, string content)
+ {
+ using var writer = new StreamWriter(archive.CreateEntry(path).Open(), Encoding.UTF8);
+ writer.Write(content);
+ }
+}
diff --git a/tests/Testcontainers.FlociAz.Tests/FlociAzContainerTest.cs b/tests/Testcontainers.FlociAz.Tests/FlociAzContainerTest.cs
new file mode 100644
index 000000000..e28f941dd
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/FlociAzContainerTest.cs
@@ -0,0 +1,515 @@
+namespace Testcontainers.FlociAz;
+
+public sealed partial class FlociAzContainerTest(FlociAzContainerTest.FlociAzFixture fixture)
+ : IClassFixture
+{
+ private const string AzureService = "Service";
+
+ public static TheoryData ArmResources
+ => new()
+ {
+ {
+ "Microsoft.Network/virtualNetworks/{name}?api-version=2024-05-01",
+ """{"location":"eastus","properties":{"addressSpace":{"addressPrefixes":["10.0.0.0/16"]}}}""",
+ "properties.provisioningState",
+ "Succeeded",
+ true
+ },
+ {
+ "Microsoft.Compute/virtualMachines/{name}?api-version=2024-11-01",
+ """{"location":"eastus","properties":{"hardwareProfile":{"vmSize":"Standard_B1s"},"storageProfile":{"imageReference":{"publisher":"Canonical","offer":"ubuntu","sku":"22_04-lts","version":"latest"}},"osProfile":{"computerName":"{name}","adminUsername":"azureuser"},"networkProfile":{"networkInterfaces":[]}}}""",
+ "properties.hardwareProfile.vmSize",
+ "Standard_B1s",
+ true
+ },
+ {
+ "Microsoft.Sql/servers/{name}?api-version=2021-11-01",
+ """{"location":"eastus","properties":{"administratorLogin":"sa","administratorLoginPassword":"FlociAz_Strong123!"}}""",
+ "properties.administratorLogin",
+ "sa",
+ true
+ },
+ {
+ "Microsoft.ContainerService/managedClusters/{name}?api-version=2024-04-01",
+ """{"location":"eastus","properties":{"kubernetesVersion":"1.29","dnsPrefix":"{name}","agentPoolProfiles":[{"name":"nodepool1","count":1,"vmSize":"Standard_DS2_v2","osType":"Linux","mode":"System"}]}}""",
+ "properties.dnsPrefix",
+ "{name}",
+ true
+ },
+ {
+ "Microsoft.ContainerRegistry/registries/{name}?api-version=2023-07-01",
+ """{"location":"eastus","sku":{"name":"Basic"},"properties":{"adminUserEnabled":true}}""",
+ "sku.name",
+ "Basic",
+ true
+ },
+ {
+ "Microsoft.Cache/redis/{name}?api-version=2024-11-01",
+ """{"location":"eastus","properties":{"sku":{"name":"Basic","family":"C","capacity":0},"enableNonSslPort":true}}""",
+ "properties.sku.name",
+ "Basic",
+ true
+ },
+ {
+ "Microsoft.ContainerInstance/containerGroups/{name}?api-version=2023-05-01",
+ """{"location":"eastus","properties":{"osType":"Linux","containers":[{"name":"app","properties":{"image":"alpine:3.20","resources":{"requests":{"cpu":1,"memoryInGB":1}}}}]}}""",
+ "properties.osType",
+ "Linux",
+ true
+ },
+ {
+ "Microsoft.ApiManagement/service/{name}?api-version=2024-05-01",
+ """{"location":"eastus","sku":{"name":"Developer","capacity":1},"properties":{"publisherEmail":"dev@example.com","publisherName":"FlociAz"}}""",
+ "properties.publisherName",
+ "FlociAz",
+ true
+ },
+ {
+ "Microsoft.ManagedIdentity/userAssignedIdentities/{name}?api-version=2024-11-30",
+ """{"location":"eastus"}""",
+ "location",
+ "eastus",
+ true
+ },
+ {
+ "Microsoft.EventGrid/topics/{name}?api-version=2023-12-15-preview",
+ """{"location":"eastus","properties":{}}""",
+ "properties.provisioningState",
+ "Succeeded",
+ true
+ },
+ {
+ "Microsoft.OperationalInsights/workspaces/{name}?api-version=2023-09-01",
+ """{"location":"eastus","properties":{}}""",
+ "properties.provisioningState",
+ "Succeeded",
+ false
+ },
+ {
+ "Microsoft.Communication/communicationServices/{name}?api-version=2023-04-01",
+ """{"location":"global","properties":{"dataLocation":"United States"}}""",
+ "properties.dataLocation",
+ "United States",
+ true
+ },
+ {
+ "Microsoft.DBforPostgreSQL/flexibleServers/{name}?api-version=2025-08-01",
+ """{"location":"eastus","sku":{"name":"Standard_B1ms","tier":"Burstable"},"properties":{"administratorLogin":"psqladmin","administratorLoginPassword":"FlociAz_Strong123!","version":"16","storage":{"storageSizeGB":32}}}""",
+ "properties.administratorLogin",
+ "psqladmin",
+ true
+ },
+ };
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task ResolvingSidecarPortRequiresDockerSocket()
+ {
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ _ = await fixture.Container.GetSidecarMappedPublicPortAsync("sidecar", 1234, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ });
+
+ Assert.Contains(nameof(FlociAzBuilder.WithDockerSocket), exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "blob")]
+ public async Task DownloadBlobReturnsUploadedBlob()
+ {
+ // Given
+ var content = Guid.NewGuid().ToString("D");
+
+ var client = new BlobServiceClient(fixture.Container.GetConnectionString());
+
+ var containerClient = client.GetBlobContainerClient(Guid.NewGuid().ToString("D"));
+
+ var blobClient = containerClient.GetBlobClient(Guid.NewGuid().ToString("D"));
+
+ // When
+ _ = await containerClient.CreateAsync(cancellationToken: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ _ = await blobClient.UploadAsync(BinaryData.FromString(content), cancellationToken: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ var downloadResult = await blobClient.DownloadContentAsync(TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(content, downloadResult.Value.Content.ToString());
+ Assert.Equal(fixture.Container.GetConnectionString(), fixture.Container.GetConnectionString(ConnectionMode.Host));
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "queue")]
+ public async Task ReceiveMessageReturnsSentMessage()
+ {
+ // Given
+ var message = Guid.NewGuid().ToString("D");
+
+ var client = new QueueServiceClient(fixture.Container.GetConnectionString());
+
+ var queueClient = client.GetQueueClient(Guid.NewGuid().ToString("D"));
+
+ // When
+ _ = await queueClient.CreateAsync(cancellationToken: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ _ = await queueClient.SendMessageAsync(message, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ var receivedMessage = await queueClient.ReceiveMessageAsync(cancellationToken: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(message, receivedMessage.Value.MessageText);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "table")]
+ public async Task GetEntityReturnsAddedEntity()
+ {
+ // Given
+ var partitionKey = Guid.NewGuid().ToString("D");
+
+ var rowKey = Guid.NewGuid().ToString("D");
+
+ var client = new TableServiceClient(fixture.Container.GetConnectionString());
+
+ var tableClient = client.GetTableClient("Table" + Guid.NewGuid().ToString("N"));
+
+ // When
+ _ = await tableClient.CreateAsync(TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ _ = await tableClient.AddEntityAsync(new TableEntity(partitionKey, rowKey), TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ var entityResponse = await tableClient.GetEntityAsync(partitionKey, rowKey, cancellationToken: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(rowKey, entityResponse.Value.RowKey);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "appconfig")]
+ public async Task GetConfigurationReturnsSetValue()
+ {
+ // Given
+ var key = Guid.NewGuid().ToString("D");
+ var value = Guid.NewGuid().ToString("D");
+ using var client = fixture.CreateHttpClient("appconfig");
+
+ // When
+ using var setResponse = await client.PutAsJsonAsync($"kv/{key}?api-version=2024-09-01", new { value }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(setResponse).ConfigureAwait(true);
+
+ using var getResponse = await client.GetAsync($"kv/{key}?api-version=2024-09-01", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(getResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(value, document.RootElement.GetProperty("value").GetString());
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "cosmos")]
+ public async Task GetDocumentReturnsCreatedDocument()
+ {
+ // Given
+ var database = "db" + Guid.NewGuid().ToString("N");
+ var documentId = Guid.NewGuid().ToString("D");
+ using var client = fixture.CreateHttpClient("cosmos");
+
+ // When
+ using var databaseResponse = await client.PostAsJsonAsync("dbs", new { id = database }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(databaseResponse).ConfigureAwait(true);
+
+ using var collectionResponse = await client.PostAsJsonAsync($"dbs/{database}/colls", new { id = "items", partitionKey = new { paths = new[] { "/category" }, kind = "Hash" } }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(collectionResponse).ConfigureAwait(true);
+
+ using var createRequest = new HttpRequestMessage(HttpMethod.Post, $"dbs/{database}/colls/items/docs");
+ createRequest.Headers.Add("x-ms-documentdb-partitionkey", "[\"test\"]");
+ createRequest.Content = JsonContent.Create(new { id = documentId, category = "test", value = "created" });
+ using var createResponse = await client.SendAsync(createRequest, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(createResponse).ConfigureAwait(true);
+
+ using var getRequest = new HttpRequestMessage(HttpMethod.Get, $"dbs/{database}/colls/items/docs/{documentId}");
+ getRequest.Headers.Add("x-ms-documentdb-partitionkey", "[\"test\"]");
+ using var getResponse = await client.SendAsync(getRequest, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(getResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal("created", document.RootElement.GetProperty("value").GetString());
+ Assert.Contains(fixture.Container.GetServiceEndpoint("cosmos"), fixture.Container.GetCosmosConnectionString());
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "keyvault")]
+ public async Task GetSecretReturnsSetSecret()
+ {
+ // Given
+ var secretName = "secret-" + Guid.NewGuid().ToString("N");
+ var secretValue = Guid.NewGuid().ToString("D");
+ using var client = fixture.CreateHttpClient("keyvault");
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "fake");
+
+ // When
+ using var setResponse = await client.PutAsJsonAsync($"secrets/{secretName}?api-version=7.4", new { value = secretValue }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(setResponse).ConfigureAwait(true);
+
+ using var getResponse = await client.GetAsync($"secrets/{secretName}?api-version=7.4", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(getResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(secretValue, document.RootElement.GetProperty("value").GetString());
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "functions")]
+ public async Task GetFunctionAppReturnsCreatedApp()
+ {
+ // Given
+ var appName = "app-" + Guid.NewGuid().ToString("N");
+ using var client = fixture.CreateHttpClient("functions");
+
+ // When
+ using var createResponse = await client.PutAsJsonAsync($"admin/apps/{appName}", new { runtime = "dotnet" }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(createResponse).ConfigureAwait(true);
+
+ using var getResponse = await client.GetAsync($"admin/apps/{appName}", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(getResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(appName, document.RootElement.GetProperty("name").GetString());
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "servicebus")]
+ public async Task GetServiceBusNamespaceReturnsCreatedNamespace()
+ {
+ // Given
+ var namespaceName = "sb-" + Guid.NewGuid().ToString("N");
+ using var client = fixture.CreateHttpClient("servicebus");
+
+ // When
+ using var createResponse = await client.PutAsJsonAsync($"namespaces/{namespaceName}", new { }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(createResponse).ConfigureAwait(true);
+
+ using var getResponse = await client.GetAsync($"namespaces/{namespaceName}", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(getResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(namespaceName, document.RootElement.GetProperty("name").GetString());
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "entra")]
+ public async Task TokenEndpointReturnsAccessToken()
+ {
+ // Given
+ using var client = fixture.CreateHttpClient();
+ using var content = new FormUrlEncodedContent(new Dictionary
+ {
+ { "grant_type", "client_credentials" },
+ { "client_id", "11111111-1111-1111-1111-111111111111" },
+ { "client_secret", "floci-az-dev-secret" },
+ { "scope", "api://resource/.default" },
+ });
+
+ // When
+ using var response = await client.PostAsync("00000000-0000-0000-0000-000000000002/oauth2/v2.0/token", content, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(response).ConfigureAwait(true);
+
+ // Then
+ Assert.False(string.IsNullOrEmpty(document.RootElement.GetProperty("access_token").GetString()));
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "managedidentity")]
+ public async Task ImdsReturnsAccessToken()
+ {
+ // Given
+ using var client = fixture.CreateHttpClient();
+ using var request = new HttpRequestMessage(HttpMethod.Get, "metadata/identity/oauth2/token?resource=https%3A%2F%2Fmanagement.azure.com%2F&api-version=2018-02-01");
+ request.Headers.Add("Metadata", "true");
+
+ // When
+ using var response = await client.SendAsync(request, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(response).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal("Bearer", document.RootElement.GetProperty("token_type").GetString());
+ Assert.False(string.IsNullOrEmpty(document.RootElement.GetProperty("access_token").GetString()));
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "email")]
+ public async Task InspectionMailboxReturnsSentEmail()
+ {
+ // Given
+ var subject = Guid.NewGuid().ToString("D");
+ using var client = fixture.CreateHttpClient();
+ var message = new
+ {
+ senderAddress = "DoNotReply@example.com",
+ content = new { subject, plainText = "Hello from Testcontainers" },
+ recipients = new { to = new[] { new { address = "dev@example.com" } } },
+ };
+
+ // When
+ using var sendResponse = await client.PostAsJsonAsync("./emails:send?api-version=2023-03-31", message, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(sendResponse).ConfigureAwait(true);
+
+ using var mailboxResponse = await client.GetAsync("emailMessages", TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(mailboxResponse).ConfigureAwait(true);
+
+ // Then
+ var email = Assert.Single(document.RootElement.GetProperty("value").EnumerateArray());
+ Assert.Equal(subject, email.GetProperty("subject").GetString());
+ }
+
+ [Theory]
+ [MemberData(nameof(ArmResources))]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "arm")]
+ public async Task ArmResourceSupportsLifecycle(string resourcePath, string payload, string expectedProperty, string expectedValue, bool supportsList)
+ {
+ // Given
+ var resourceName = "tc" + Guid.NewGuid().ToString("N");
+ var path = $"subscriptions/{FlociAzFixture.SubscriptionId}/resourceGroups/{FlociAzFixture.ResourceGroup}/providers/{resourcePath}"
+ .Replace("{name}", resourceName, StringComparison.Ordinal);
+ var queryIndex = resourcePath.IndexOf('?', StringComparison.Ordinal);
+ var resourceIndex = resourcePath.IndexOf("/{name}", StringComparison.Ordinal);
+ var collectionResourcePath = resourcePath.Substring(0, resourceIndex) + resourcePath.Substring(queryIndex);
+ var collectionPath = $"subscriptions/{FlociAzFixture.SubscriptionId}/resourceGroups/{FlociAzFixture.ResourceGroup}/providers/{collectionResourcePath}";
+ using var client = fixture.CreateHttpClient();
+
+ // When
+ using var content = new StringContent(payload.Replace("{name}", resourceName, StringComparison.Ordinal), Encoding.UTF8, "application/json");
+ using var createResponse = await client.PutAsync(path, content, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(createResponse).ConfigureAwait(true);
+
+ using var getResponse = await client.GetAsync(path, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var document = await ReadJsonAsync(getResponse).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(resourceName, document.RootElement.GetProperty("name").GetString());
+ Assert.Equal(expectedValue.Replace("{name}", resourceName, StringComparison.Ordinal), GetProperty(document.RootElement, expectedProperty).GetString());
+
+ if (supportsList)
+ {
+ using var listResponse = await client.GetAsync(collectionPath, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ using var listDocument = await ReadJsonAsync(listResponse).ConfigureAwait(true);
+ Assert.Contains(listDocument.RootElement.GetProperty("value").EnumerateArray(), resource => resource.GetProperty("name").GetString() == resourceName);
+ }
+
+ using var deleteResponse = await client.DeleteAsync(path, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(deleteResponse).ConfigureAwait(true);
+
+ using var deletedResponse = await client.GetAsync(path, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ Assert.Equal(HttpStatusCode.NotFound, deletedResponse.StatusCode);
+ }
+
+ private static JsonElement GetProperty(JsonElement element, string path)
+ {
+ foreach (var segment in path.Split('.'))
+ {
+ element = element.GetProperty(segment);
+ }
+
+ return element;
+ }
+
+ private static async Task AssertSuccessAsync(HttpResponseMessage response)
+ {
+ if (response.IsSuccessStatusCode)
+ {
+ return;
+ }
+
+ var content = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ Assert.Fail($"{response.RequestMessage?.Method} {response.RequestMessage?.RequestUri} returned {(int)response.StatusCode}: {content}");
+ }
+
+ private static async Task ReadJsonAsync(HttpResponseMessage response)
+ {
+ await AssertSuccessAsync(response).ConfigureAwait(true);
+ return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken).ConfigureAwait(true), cancellationToken: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ }
+
+ public sealed class FlociAzFixture : IAsyncLifetime
+ {
+ public const string SubscriptionId = "00000000-0000-0000-0000-000000000001";
+
+ public const string ResourceGroup = "testcontainers";
+
+ public FlociAzContainer Container { get; }
+ = new FlociAzBuilder(TestSession.GetImageFromDockerfile())
+ .WithEnvironment("FLOCI_AZ_SERVICES_EVENT_HUB_ENABLED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_EVENT_HUB_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_FUNCTIONS_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_POSTGRES_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_AKS_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_ACR_MOCKED", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_REDIS_MOCKED", "true")
+ .Build();
+
+ public async ValueTask InitializeAsync()
+ {
+ await Container.StartAsync()
+ .ConfigureAwait(false);
+
+ using var client = CreateHttpClient();
+ using var response = await client.PutAsJsonAsync($"subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroup}?api-version=2021-04-01", new { location = "eastus" }, TestContext.Current.CancellationToken)
+ .ConfigureAwait(false);
+ await AssertSuccessAsync(response).ConfigureAwait(false);
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ return Container.DisposeAsync();
+ }
+
+ public HttpClient CreateHttpClient(string service = null)
+ {
+ var endpoint = service is null ? Container.GetEndpoint() : Container.GetServiceEndpoint(service);
+ return new HttpClient { BaseAddress = new Uri(endpoint) };
+ }
+ }
+}
diff --git a/tests/Testcontainers.FlociAz.Tests/FlociAzFunctionsSidecarTest.cs b/tests/Testcontainers.FlociAz.Tests/FlociAzFunctionsSidecarTest.cs
new file mode 100644
index 000000000..274192ef5
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/FlociAzFunctionsSidecarTest.cs
@@ -0,0 +1,103 @@
+namespace Testcontainers.FlociAz;
+
+[Collection(nameof(FlociAzSidecarCollection))]
+public sealed class FlociAzFunctionsSidecarTest(FlociAzFunctionsSidecarTest.FlociAzFunctionsFixture fixture)
+ : IClassFixture
+{
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait("Service", "functions-real")]
+ public async Task FunctionSidecarExecutesDeployedCode()
+ {
+ // Given
+ var appName = "app" + Guid.NewGuid().ToString("N");
+ const string functionName = "HttpTrigger";
+ using var client = new HttpClient { BaseAddress = new Uri(fixture.Container.GetServiceEndpoint("functions")) };
+ using var package = CreateFunctionPackage();
+
+ // When
+ using var appResponse = await client.PutAsJsonAsync($"admin/apps/{appName}", new { runtime = "node" }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(appResponse).ConfigureAwait(true);
+ using var deployResponse = await client.PutAsJsonAsync($"admin/apps/{appName}/functions/{functionName}", new { handler = "index.handler", zipBase64 = Convert.ToBase64String(package.ToArray()) }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deployResponse).ConfigureAwait(true);
+ using var invokeResponse = await InvokeWhenReadyAsync(client, appName, functionName).ConfigureAwait(true);
+ var responseBody = await invokeResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ // Then
+ await AssertSuccessAsync(invokeResponse).ConfigureAwait(true);
+ Assert.Contains("compatible", responseBody, StringComparison.OrdinalIgnoreCase);
+
+ using var deleteResponse = await client.DeleteAsync($"admin/apps/{appName}", TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteResponse).ConfigureAwait(true);
+ }
+
+ private static MemoryStream CreateFunctionPackage()
+ {
+ var stream = new MemoryStream();
+ using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
+ {
+ WriteEntry(archive, "function.json", "{\"scriptFile\":\"index.js\",\"bindings\":[{\"authLevel\":\"anonymous\",\"type\":\"httpTrigger\",\"direction\":\"in\",\"name\":\"req\",\"methods\":[\"get\",\"post\"]},{\"type\":\"http\",\"direction\":\"out\",\"name\":\"res\"}]}");
+ WriteEntry(archive, "index.js", "module.exports = async function (context) { context.res = { status: 200, body: 'compatible' }; }; ");
+ }
+
+ stream.Position = 0;
+ return stream;
+ }
+
+ private static void WriteEntry(ZipArchive archive, string path, string content)
+ {
+ using var writer = new StreamWriter(archive.CreateEntry(path).Open(), Encoding.UTF8);
+ writer.Write(content);
+ }
+
+ private static async Task AssertSuccessAsync(HttpResponseMessage response)
+ {
+ if (response.IsSuccessStatusCode)
+ {
+ return;
+ }
+
+ var content = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+ Assert.Fail($"{response.RequestMessage?.Method} {response.RequestMessage?.RequestUri} returned {(int)response.StatusCode}: {content}");
+ }
+
+ private static async Task InvokeWhenReadyAsync(HttpClient client, string appName, string functionName)
+ {
+ var timeout = DateTime.UtcNow.AddMinutes(2);
+ HttpResponseMessage response;
+
+ do
+ {
+ response = await client.PostAsJsonAsync($"api/{appName}/{functionName}", new { }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ if (response.StatusCode != HttpStatusCode.NotFound)
+ {
+ return response;
+ }
+
+ response.Dispose();
+ await Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken).ConfigureAwait(true);
+ }
+ while (DateTime.UtcNow < timeout);
+
+ return await client.PostAsJsonAsync($"api/{appName}/{functionName}", new { }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ }
+
+ public sealed class FlociAzFunctionsFixture : IAsyncLifetime
+ {
+ public FlociAzContainer Container { get; }
+ = new FlociAzBuilder(TestSession.GetImageFromDockerfile())
+ .WithDockerSocket()
+ .WithEnvironment("FLOCI_AZ_SERVICES_FUNCTIONS_MOCKED", "false")
+ .Build();
+
+ public async ValueTask InitializeAsync()
+ {
+ await Container.StartAsync().ConfigureAwait(false);
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ return Container.DisposeAsync();
+ }
+ }
+}
diff --git a/tests/Testcontainers.FlociAz.Tests/FlociAzSidecarTest.cs b/tests/Testcontainers.FlociAz.Tests/FlociAzSidecarTest.cs
new file mode 100644
index 000000000..4bc10d471
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/FlociAzSidecarTest.cs
@@ -0,0 +1,198 @@
+namespace Testcontainers.FlociAz;
+
+[Collection(nameof(FlociAzSidecarCollection))]
+public sealed class FlociAzSidecarTest(FlociAzSidecarTest.FlociAzSidecarFixture fixture)
+ : IClassFixture
+{
+ private const string AzureService = "Service";
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "servicebus")]
+ public async Task ServiceBusSendsAndReceivesMessage()
+ {
+ // Given
+ var queueName = "queue-" + Guid.NewGuid().ToString("N");
+ using var managementClient = fixture.CreateHttpClient("servicebus");
+ using var createResponse = await managementClient.PutAsync(queueName, new StringContent("", Encoding.UTF8, "application/atom+xml"), TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+ await AssertSuccessAsync(createResponse).ConfigureAwait(true);
+
+ await using var client = new ServiceBusClient(CreateAmqpConnectionString(fixture.Container.Hostname, 5673));
+ await using var sender = client.CreateSender(queueName);
+ await using var receiver = client.CreateReceiver(queueName);
+ var messageBody = Guid.NewGuid().ToString("D");
+
+ // When
+ await sender.SendMessageAsync(new ServiceBusMessage(messageBody), TestContext.Current.CancellationToken).ConfigureAwait(true);
+ var message = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(20), TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ // Then
+ Assert.NotNull(message);
+ Assert.Equal(messageBody, message.Body.ToString());
+ await receiver.CompleteMessageAsync(message, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "redis")]
+ public async Task RedisSidecarStoresAndReturnsValue()
+ {
+ // Given
+ var cacheName = "redis" + Guid.NewGuid().ToString("N");
+ var path = fixture.GetArmPath($"Microsoft.Cache/redis/{cacheName}?api-version=2024-11-01");
+ using var client = fixture.CreateHttpClient();
+ var payload = new { location = "eastus", properties = new { sku = new { name = "Basic", family = "C", capacity = 0 }, enableNonSslPort = true } };
+
+ // When
+ using var createResponse = await client.PutAsJsonAsync(path, payload, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ using var cache = await ReadJsonAsync(createResponse).ConfigureAwait(true);
+ var properties = cache.RootElement.GetProperty("properties");
+ var sidecarHostname = properties.GetProperty("hostName").GetString();
+ var privatePort = properties.GetProperty("port").GetUInt16();
+ var publicPort = await fixture.Container.GetSidecarMappedPublicPortAsync(sidecarHostname, privatePort, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ var password = properties.GetProperty("accessKeys").GetProperty("primaryKey").GetString();
+
+ await using var connection = await ConnectionMultiplexer.ConnectAsync($"{fixture.Container.Hostname}:{publicPort},password={password},abortConnect=false").ConfigureAwait(true);
+ var database = connection.GetDatabase();
+ var key = Guid.NewGuid().ToString("D");
+ var value = Guid.NewGuid().ToString("D");
+ _ = await database.StringSetAsync(key, value).ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(value, await database.StringGetAsync(key).ConfigureAwait(true));
+
+ using var deleteResponse = await client.DeleteAsync(path, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteResponse).ConfigureAwait(true);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "postgresql")]
+ public async Task PostgreSqlSidecarExecutesQuery()
+ {
+ // Given
+ var serverName = "pg" + Guid.NewGuid().ToString("N");
+ var path = fixture.GetArmPath($"Microsoft.DBforPostgreSQL/flexibleServers/{serverName}?api-version=2025-08-01");
+ using var client = fixture.CreateHttpClient();
+ var payload = new
+ {
+ location = "eastus",
+ sku = new { name = "Standard_B1ms", tier = "Burstable" },
+ properties = new { administratorLogin = "psqladmin", administratorLoginPassword = "FlociAz_Strong123!", version = "16", storage = new { storageSizeGB = 32 } },
+ };
+
+ // When
+ using var createResponse = await client.PutAsJsonAsync(path, payload, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(createResponse).ConfigureAwait(true);
+ using var postgresClient = fixture.CreateHttpClient("postgres");
+ using var connectResponse = await postgresClient.GetAsync($"flexibleServers/{serverName}/connect", TestContext.Current.CancellationToken).ConfigureAwait(true);
+ using var connectionInfo = await ReadJsonAsync(connectResponse).ConfigureAwait(true);
+ var sidecarHostname = connectionInfo.RootElement.GetProperty("host").GetString();
+ var privatePort = connectionInfo.RootElement.GetProperty("port").GetUInt16();
+ var publicPort = await fixture.Container.GetSidecarMappedPublicPortAsync(sidecarHostname, privatePort, TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ var connectionString = $"Host={fixture.Container.Hostname};Port={publicPort};Database=postgres;Username=psqladmin;Password=FlociAz_Strong123!;SSL Mode=Disable";
+ await using var connection = new NpgsqlConnection(connectionString);
+ await connection.OpenAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await using var command = new NpgsqlCommand("SELECT 42", connection);
+
+ // Then
+ Assert.Equal(42, await command.ExecuteScalarAsync(TestContext.Current.CancellationToken).ConfigureAwait(true));
+
+ using var deleteResponse = await client.DeleteAsync(path, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ await AssertSuccessAsync(deleteResponse).ConfigureAwait(true);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ [Trait(AzureService, "acr")]
+ public async Task ContainerRegistrySidecarExposesRegistryApi()
+ {
+ // Given
+ var registryName = "acr" + Guid.NewGuid().ToString("N");
+ var path = fixture.GetArmPath($"Microsoft.ContainerRegistry/registries/{registryName}?api-version=2023-07-01");
+ using var client = fixture.CreateHttpClient();
+
+ // When
+ using var createResponse = await client.PutAsJsonAsync(path, new { location = "eastus", sku = new { name = "Basic" }, properties = new { adminUserEnabled = true } }, TestContext.Current.CancellationToken).ConfigureAwait(true);
+ using var registry = await ReadJsonAsync(createResponse).ConfigureAwait(true);
+ var loginServer = registry.RootElement.GetProperty("properties").GetProperty("loginServer").GetString();
+ var registryUri = new Uri(Uri.UriSchemeHttp + Uri.SchemeDelimiter + loginServer);
+ var sidecarHostname = registryUri.Host;
+ var privatePort = checked((ushort)registryUri.Port);
+ var publicPort = await fixture.Container.GetSidecarMappedPublicPortAsync(sidecarHostname, privatePort, TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ using var registryClient = new HttpClient { BaseAddress = new UriBuilder(Uri.UriSchemeHttp, fixture.Container.Hostname, publicPort).Uri };
+ using var apiResponse = await registryClient.GetAsync("v2/", TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ // Then
+ await AssertSuccessAsync(apiResponse).ConfigureAwait(true);
+ Assert.True(apiResponse.Headers.Contains("Docker-Distribution-Api-Version"));
+ }
+
+ private static string CreateAmqpConnectionString(string hostname, ushort port)
+ {
+ return $"Endpoint=sb://{hostname}:{port};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=devkey;UseDevelopmentEmulator=true;";
+ }
+
+ private static async Task AssertSuccessAsync(HttpResponseMessage response)
+ {
+ if (response.IsSuccessStatusCode)
+ {
+ return;
+ }
+
+ var content = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+ Assert.Fail($"{response.RequestMessage?.Method} {response.RequestMessage?.RequestUri} returned {(int)response.StatusCode}: {content}");
+ }
+
+ private static async Task ReadJsonAsync(HttpResponseMessage response)
+ {
+ await AssertSuccessAsync(response).ConfigureAwait(true);
+ return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken).ConfigureAwait(true), cancellationToken: TestContext.Current.CancellationToken).ConfigureAwait(true);
+ }
+
+ public sealed class FlociAzSidecarFixture : IAsyncLifetime
+ {
+ private const string SubscriptionId = "00000000-0000-0000-0000-000000000003";
+
+ private const string ResourceGroup = "sidecars";
+
+ public FlociAzContainer Container { get; }
+ = new FlociAzBuilder(TestSession.GetImageFromDockerfile())
+ .WithDockerSocket()
+ .WithEnvironment("FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_SERVICE_BUS_START_ON_BOOT", "true")
+ .WithEnvironment("FLOCI_AZ_SERVICES_POSTGRES_MOCKED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_ACR_MOCKED", "false")
+ .WithEnvironment("FLOCI_AZ_SERVICES_REDIS_MOCKED", "false")
+ .Build();
+
+ public async ValueTask InitializeAsync()
+ {
+ await Container.StartAsync().ConfigureAwait(false);
+ using var client = CreateHttpClient();
+ using var response = await client.PutAsJsonAsync($"subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroup}?api-version=2021-04-01", new { location = "eastus" }, TestContext.Current.CancellationToken).ConfigureAwait(false);
+ await AssertSuccessAsync(response).ConfigureAwait(false);
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ return Container.DisposeAsync();
+ }
+
+ public HttpClient CreateHttpClient(string service = null)
+ {
+ return new HttpClient { BaseAddress = new Uri(service is null ? Container.GetEndpoint() : Container.GetServiceEndpoint(service)) };
+ }
+
+ public string GetArmPath(string resourcePath)
+ {
+ return $"subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroup}/providers/{resourcePath}";
+ }
+ }
+}
+
+[CollectionDefinition(nameof(FlociAzSidecarCollection), DisableParallelization = true)]
+public sealed class FlociAzSidecarCollection;
diff --git a/tests/Testcontainers.FlociAz.Tests/Testcontainers.FlociAz.Tests.csproj b/tests/Testcontainers.FlociAz.Tests/Testcontainers.FlociAz.Tests.csproj
new file mode 100644
index 000000000..a1eda61df
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/Testcontainers.FlociAz.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+ net10.0
+ false
+ false
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/tests/Testcontainers.FlociAz.Tests/Usings.cs b/tests/Testcontainers.FlociAz.Tests/Usings.cs
new file mode 100644
index 000000000..7bbf43aaa
--- /dev/null
+++ b/tests/Testcontainers.FlociAz.Tests/Usings.cs
@@ -0,0 +1,21 @@
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.IO.Compression;
+global using System.Net;
+global using System.Net.Http;
+global using System.Net.Http.Headers;
+global using System.Net.Http.Json;
+global using System.Text;
+global using System.Text.Json;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Azure.Data.Tables;
+global using Azure.Messaging.ServiceBus;
+global using Azure.Storage.Blobs;
+global using Azure.Storage.Queues;
+global using DotNet.Testcontainers.Commons;
+global using DotNet.Testcontainers.Configurations;
+global using Npgsql;
+global using StackExchange.Redis;
+global using Xunit;