From 24a5540411913a7d7d1fad8631056362bc461bf3 Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Thu, 31 Jul 2025 12:42:48 +0200 Subject: [PATCH 1/8] feat(spicedb): add testcontainer module for SpiceDB --- .github/workflows/cicd.yml | 3 +- Directory.Packages.props | 179 +++++++++--------- Testcontainers.sln | 18 ++ docs/modules/index.md | 3 +- docs/modules/spicedb.md | 89 +++++++++ src/Testcontainers.SpiceDB/.editorconfig | 1 + src/Testcontainers.SpiceDB/SpiceDBBuilder.cs | 66 +++++++ .../SpiceDBConfiguration.cs | 56 ++++++ .../SpiceDBContainer.cs | 43 +++++ .../Testcontainers.SpiceDB.csproj | 18 ++ src/Testcontainers.SpiceDB/Usings.cs | 10 + .../.editorconfig | 1 + .../SpiceDBContainerTest.cs | 85 +++++++++ .../Testcontainers.SpiceDB.Tests.csproj | 18 ++ tests/Testcontainers.SpiceDB.Tests/Usings.cs | 4 + 15 files changed, 504 insertions(+), 90 deletions(-) create mode 100644 docs/modules/spicedb.md create mode 100644 src/Testcontainers.SpiceDB/.editorconfig create mode 100644 src/Testcontainers.SpiceDB/SpiceDBBuilder.cs create mode 100644 src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs create mode 100644 src/Testcontainers.SpiceDB/SpiceDBContainer.cs create mode 100644 src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj create mode 100644 src/Testcontainers.SpiceDB/Usings.cs create mode 100644 tests/Testcontainers.SpiceDB.Tests/.editorconfig create mode 100644 tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs create mode 100644 tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj create mode 100644 tests/Testcontainers.SpiceDB.Tests/Usings.cs diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index c6c2ec2a2..687a98b81 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -95,7 +95,8 @@ jobs: { name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.XunitV3", runs-on: "ubuntu-22.04" } + { name: "Testcontainers.XunitV3", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.SpiceDb", runs-on: "ubuntu-22.04" } ] runs-on: ${{ matrix.test-projects.runs-on }} diff --git a/Directory.Packages.props b/Directory.Packages.props index 3ab6849e8..17bd460fa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,90 +1,93 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Testcontainers.sln b/Testcontainers.sln index e09e2b093..e82e901d3 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -257,6 +257,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Xunit.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.XunitV3.Tests", "tests\Testcontainers.XunitV3.Tests\Testcontainers.XunitV3.Tests.csproj", "{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.SpiceDB", "src\Testcontainers.SpiceDB\Testcontainers.SpiceDB.csproj", "{64B27088-14DC-4CA2-B24E-5D0D5BA14355}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.SpiceDB.Tests", "tests\Testcontainers.SpiceDB.Tests\Testcontainers.SpiceDB.Tests.csproj", "{21D155EB-A843-4D4D-84E1-5C913217BE89}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -751,6 +755,18 @@ Global {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.Build.0 = Release|Any CPU + {64B27088-14DC-4CA2-B24E-5D0D5BA14355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64B27088-14DC-4CA2-B24E-5D0D5BA14355}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64B27088-14DC-4CA2-B24E-5D0D5BA14355}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64B27088-14DC-4CA2-B24E-5D0D5BA14355}.Release|Any CPU.Build.0 = Release|Any CPU + {A9F554E5-9183-4F8A-B226-58DD46BB2060}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9F554E5-9183-4F8A-B226-58DD46BB2060}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9F554E5-9183-4F8A-B226-58DD46BB2060}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9F554E5-9183-4F8A-B226-58DD46BB2060}.Release|Any CPU.Build.0 = Release|Any CPU + {21D155EB-A843-4D4D-84E1-5C913217BE89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21D155EB-A843-4D4D-84E1-5C913217BE89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21D155EB-A843-4D4D-84E1-5C913217BE89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21D155EB-A843-4D4D-84E1-5C913217BE89}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -878,5 +894,7 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {64B27088-14DC-4CA2-B24E-5D0D5BA14355} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {21D155EB-A843-4D4D-84E1-5C913217BE89} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/docs/modules/index.md b/docs/modules/index.md index 254a2d9fe..476749cd2 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -20,7 +20,7 @@ await moduleNameContainer.StartAsync(); We will add module-specific documentations soon. | Module | Image | NuGet | Source | -|-------------------|---------------------------------------------------------------------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| ----------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | ActiveMQ Artemis | `apache/activemq-artemis:2.31.2` | [NuGet](https://www.nuget.org/packages/Testcontainers.ActiveMq) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.ActiveMq) | | ArangoDB | `arangodb:3.11.5` | [NuGet](https://www.nuget.org/packages/Testcontainers.ArangoDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.ArangoDb) | | Azure Cosmos DB | `mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.CosmosDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.CosmosDb) | @@ -73,6 +73,7 @@ await moduleNameContainer.StartAsync(); | Typesense | `typesense/typesense:28.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Typesense) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Typesense) | | Weaviate | `semitechnologies/weaviate:1.26.14` | [NuGet](https://www.nuget.org/packages/Testcontainers.Weaviate) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Weaviate) | | WebDriver | `selenium/standalone-chrome:110.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.WebDriver) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.WebDriver) | +| SpiceDB | `authzed/spicedb:v1.45.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.SpiceDB) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.SpiceDB) | ## Implement a module diff --git a/docs/modules/spicedb.md b/docs/modules/spicedb.md new file mode 100644 index 000000000..4a9498abd --- /dev/null +++ b/docs/modules/spicedb.md @@ -0,0 +1,89 @@ +# SpiceDB + +[SpiceDB](https://authzed.com/spicedb/) is an open-source, Google Zanzibar-inspired permissions database that provides a centralized service to store, compute, and validate application permissions. It enables fine-grained authorization at scale with a flexible relationship-based permission model. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.SpiceDB +``` + +You can start a SpiceDB container instance from any .NET application. To create and start a container instance with the default configuration, use the module-specific builder as shown below: + +=== "Start a SpiceDB container" +`csharp + var spicedbContainer = new SpiceDBBuilder().Build(); + await spicedbContainer.StartAsync(); + ` + +The following example utilizes the [xUnit.net](/test_frameworks/xunit_net/) module to reduce overhead by automatically managing the lifecycle of the dependent container instance. It creates and starts the container using the module-specific builder and injects it as a shared class fixture into the test class. + +=== "Usage Example" +```csharp +public sealed class SpiceDBContainerTest : IAsyncLifetime +{ +private readonly SpiceDBContainer \_spicedbContainer = new SpiceDBBuilder().Build(); + + public Task InitializeAsync() + { + return _spicedbContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _spicedbContainer.DisposeAsync().AsTask(); + } + + [Fact] + public async Task SpiceDBContainer_IsRunning_ReturnsTrue() + { + // Given + var containerState = await _spicedbContainer.GetStateAsync(); + + // When & Then + Assert.Equal(ResourceState.Running, containerState.Status); + } + } + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" +`xml + + + + + + + ` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/\_call_out_test_projects.txt" + +## Configuration + +The SpiceDB container is pre-configured with the following settings: + +- **Image**: `authzed/spicedb:v1.45.1` +- **Port**: `50051` (gRPC port) +- **Wait Strategy**: Waits for the SpiceDB CLI ping command to complete successfully + +## Connection + +SpiceDB exposes a gRPC API on port 50051. You can connect to it using the container's host and port: + +```csharp +var host = spicedbContainer.Hostname; +var port = spicedbContainer.GetMappedPublicPort(50051); +// Connect to gRPC endpoint at host:port +``` + +## Features + +- **Relationship-based permissions**: Define complex permission models using relationships between resources +- **Consistency guarantees**: Provides ACID transactions for permission checks +- **Schema management**: Define and evolve permission schemas +- **gRPC API**: High-performance API for permission operations +- **Docker-based**: Easy to run and test in containerized environments diff --git a/src/Testcontainers.SpiceDB/.editorconfig b/src/Testcontainers.SpiceDB/.editorconfig new file mode 100644 index 000000000..78b36ca08 --- /dev/null +++ b/src/Testcontainers.SpiceDB/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs b/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs new file mode 100644 index 000000000..cc5a672cb --- /dev/null +++ b/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs @@ -0,0 +1,66 @@ +namespace Testcontainers.SpiceDB; + +/// +[PublicAPI] +public sealed class SpiceDBBuilder : ContainerBuilder +{ + public const string SpiceDBImage = "authzed/spicedb:v1.45.1"; + + public const ushort SpiceDBPort = 50051; + + /// + /// Initializes a new instance of the class. + /// + public SpiceDBBuilder() + : this(new SpiceDBConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private SpiceDBBuilder(SpiceDBConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override SpiceDBConfiguration DockerResourceConfiguration { get; } + + /// + public override SpiceDBContainer Build() + { + Validate(); + return new SpiceDBContainer(DockerResourceConfiguration); + } + + /// + protected override SpiceDBBuilder Init() + { + return base.Init() + .WithImage(SpiceDBImage) + .WithPortBinding(SpiceDBPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("SpiceDB-cli", "ping")); + } + + /// + protected override SpiceDBBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SpiceDBConfiguration(resourceConfiguration)); + } + + /// + protected override SpiceDBBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new SpiceDBConfiguration(resourceConfiguration)); + } + + /// + protected override SpiceDBBuilder Merge(SpiceDBConfiguration oldValue, SpiceDBConfiguration newValue) + { + return new SpiceDBBuilder(new SpiceDBConfiguration(oldValue, newValue)); + } +} diff --git a/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs b/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs new file mode 100644 index 000000000..29a92f706 --- /dev/null +++ b/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs @@ -0,0 +1,56 @@ +namespace Testcontainers.SpiceDB; + +/// +[PublicAPI] +public sealed class SpiceDBConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public SpiceDBConfiguration(bool? tslEnabled = false) + { + TslEnabled = tslEnabled.GetValueOrDefault(false); + } + + public bool TslEnabled { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public SpiceDBConfiguration(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 SpiceDBConfiguration(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 SpiceDBConfiguration(SpiceDBConfiguration resourceConfiguration) + : this(new SpiceDBConfiguration(), 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 SpiceDBConfiguration(SpiceDBConfiguration oldValue, SpiceDBConfiguration newValue) + : base(oldValue, newValue) + { + } +} diff --git a/src/Testcontainers.SpiceDB/SpiceDBContainer.cs b/src/Testcontainers.SpiceDB/SpiceDBContainer.cs new file mode 100644 index 000000000..7063d7b5d --- /dev/null +++ b/src/Testcontainers.SpiceDB/SpiceDBContainer.cs @@ -0,0 +1,43 @@ +using System.Threading; +using Health = Grpc.Health.V1.Health; + +namespace Testcontainers.SpiceDB; + +/// +[PublicAPI] +public sealed class SpiceDBContainer : DockerContainer +{ + SpiceDBConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public SpiceDBContainer(SpiceDBConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + + public string GetGrpcConnectionString() + { + var scheme = _configuration.TslEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(SpiceDBBuilder.SpiceDBPort)); + return endpoint.ToString(); + } + + public GrpcChannel GetGrpcChannel() + { + return GrpcChannel.ForAddress(GetGrpcConnectionString()); + } + + public async Task GetStateAsync(CancellationToken cancellationToken = default) + { + var healthClient = new Health.HealthClient(GetGrpcChannel()); + var response = await healthClient.CheckAsync(new HealthCheckRequest + { + Service = string.Empty, + }, null, null, cancellationToken); + return response.Status.ToString(); + } +} diff --git a/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj b/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj new file mode 100644 index 000000000..d91941e50 --- /dev/null +++ b/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj @@ -0,0 +1,18 @@ + + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + + + + + + diff --git a/src/Testcontainers.SpiceDB/Usings.cs b/src/Testcontainers.SpiceDB/Usings.cs new file mode 100644 index 000000000..1ccf064c9 --- /dev/null +++ b/src/Testcontainers.SpiceDB/Usings.cs @@ -0,0 +1,10 @@ +global using System; +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; +global using Grpc.Health.V1; +global using Grpc.Net.Client; +global using Grpc.Core; \ No newline at end of file diff --git a/tests/Testcontainers.SpiceDB.Tests/.editorconfig b/tests/Testcontainers.SpiceDB.Tests/.editorconfig new file mode 100644 index 000000000..14403c511 --- /dev/null +++ b/tests/Testcontainers.SpiceDB.Tests/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs new file mode 100644 index 000000000..39076aa49 --- /dev/null +++ b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; + +namespace Testcontainers.SpiceDB; + +public sealed class SpiceDBContainerTest : IAsyncLifetime +{ + private readonly SpiceDBContainer _spicedbContainer = new SpiceDBBuilder().Build(); + + public async ValueTask InitializeAsync() + { + await _spicedbContainer.StartAsync() + .ConfigureAwait(false); + } + + public ValueTask DisposeAsync() + { + return _spicedbContainer.DisposeAsync(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ContainerStartsSuccessfully() + { + // Given + var containerState = await _spicedbContainer.GetStateAsync(TestContext.Current.CancellationToken) + .ConfigureAwait(false); + + // When & Then + Assert.Equal(containerState, "started"); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void ExpectedPortIsMapped() + { + // Given & When + var mappedPort = _spicedbContainer.GetMappedPublicPort(SpiceDBBuilder.SpiceDBPort); + + // Then + Assert.True(mappedPort > 0); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ExecCommandReturnsSuccessful() + { + // Given + List commands = ["spicedb-cli", "version"]; + + // When + var execResult = await _spicedbContainer.ExecAsync(commands, TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + Assert.Contains("spicedb", execResult.Stdout); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task PingCommandReturnsSuccessful() + { + // Given + List commands = ["spicedb-cli", "ping"]; + + // When + var execResult = await _spicedbContainer.ExecAsync(commands, TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void GetGrpcConnectionStringReturnsExpectedFormat() + { + // Given & When + var connectionString = _spicedbContainer.GetGrpcConnectionString(); + + // Then + // Note: This test will need to be updated once GetConnectionString() is properly implemented + Assert.NotNull(connectionString); + } +} diff --git a/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj b/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj new file mode 100644 index 000000000..7f8859de5 --- /dev/null +++ b/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj @@ -0,0 +1,18 @@ + + + net9.0 + false + false + Exe + + + + + + + + + + + + diff --git a/tests/Testcontainers.SpiceDB.Tests/Usings.cs b/tests/Testcontainers.SpiceDB.Tests/Usings.cs new file mode 100644 index 000000000..89af834f6 --- /dev/null +++ b/tests/Testcontainers.SpiceDB.Tests/Usings.cs @@ -0,0 +1,4 @@ +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using Testcontainers.SpiceDB; +global using Xunit; From b494bdde3557aca9d25831a4dda4b32b3c223a73 Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Thu, 31 Jul 2025 12:49:42 +0200 Subject: [PATCH 2/8] docs(spicedb): update container usage example and lifecycle management details --- docs/modules/spicedb.md | 43 +++++------------------------------------ 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/docs/modules/spicedb.md b/docs/modules/spicedb.md index 4a9498abd..c3ea8951d 100644 --- a/docs/modules/spicedb.md +++ b/docs/modules/spicedb.md @@ -8,18 +8,11 @@ Add the following dependency to your project file: dotnet add package Testcontainers.SpiceDB ``` -You can start a SpiceDB container instance from any .NET application. To create and start a container instance with the default configuration, use the module-specific builder as shown below: - -=== "Start a SpiceDB container" -`csharp - var spicedbContainer = new SpiceDBBuilder().Build(); - await spicedbContainer.StartAsync(); - ` - -The following example utilizes the [xUnit.net](/test_frameworks/xunit_net/) module to reduce overhead by automatically managing the lifecycle of the dependent container instance. It creates and starts the container using the module-specific builder and injects it as a shared class fixture into the test class. +You can start a SpiceDB 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 + +````csharp public sealed class SpiceDBContainerTest : IAsyncLifetime { private readonly SpiceDBContainer \_spicedbContainer = new SpiceDBBuilder().Build(); @@ -40,8 +33,7 @@ private readonly SpiceDBContainer \_spicedbContainer = new SpiceDBBuilder().Buil // Given var containerState = await _spicedbContainer.GetStateAsync(); - // When & Then - Assert.Equal(ResourceState.Running, containerState.Status); + // ... } } ``` @@ -61,29 +53,4 @@ The test example uses the following NuGet dependencies: To execute the tests, use the command `dotnet test` from a terminal. --8<-- "docs/modules/\_call_out_test_projects.txt" - -## Configuration - -The SpiceDB container is pre-configured with the following settings: - -- **Image**: `authzed/spicedb:v1.45.1` -- **Port**: `50051` (gRPC port) -- **Wait Strategy**: Waits for the SpiceDB CLI ping command to complete successfully - -## Connection - -SpiceDB exposes a gRPC API on port 50051. You can connect to it using the container's host and port: - -```csharp -var host = spicedbContainer.Hostname; -var port = spicedbContainer.GetMappedPublicPort(50051); -// Connect to gRPC endpoint at host:port -``` - -## Features - -- **Relationship-based permissions**: Define complex permission models using relationships between resources -- **Consistency guarantees**: Provides ACID transactions for permission checks -- **Schema management**: Define and evolve permission schemas -- **gRPC API**: High-performance API for permission operations -- **Docker-based**: Easy to run and test in containerized environments +```` From 7de600c99bc03ac57190ed43aa1ee653de26a34b Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Thu, 31 Jul 2025 12:56:41 +0200 Subject: [PATCH 3/8] chore: Clean up formatting in Directory.Packages.props for consistency --- Directory.Packages.props | 177 +++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 90 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 17bd460fa..0deb39ba4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,93 +1,90 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From dda1cf8b8a682162346669e3d44fc4eded6d4a0d Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Thu, 31 Jul 2025 12:57:19 +0200 Subject: [PATCH 4/8] chore: Add newline at end of file in Directory.Packages.props --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0deb39ba4..3ab6849e8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -87,4 +87,4 @@ - \ No newline at end of file + From 769a15bd92e6e6e62310c37ebb3703179d9d7d2d Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Thu, 31 Jul 2025 14:12:14 +0200 Subject: [PATCH 5/8] refactor(spicedb): remove unused gRPC channel methods and dependencies --- src/Testcontainers.SpiceDB/SpiceDBContainer.cs | 15 --------------- .../Testcontainers.SpiceDB.csproj | 1 - src/Testcontainers.SpiceDB/Usings.cs | 1 - .../SpiceDBContainerTest.cs | 12 ------------ 4 files changed, 29 deletions(-) diff --git a/src/Testcontainers.SpiceDB/SpiceDBContainer.cs b/src/Testcontainers.SpiceDB/SpiceDBContainer.cs index 7063d7b5d..56e0a6125 100644 --- a/src/Testcontainers.SpiceDB/SpiceDBContainer.cs +++ b/src/Testcontainers.SpiceDB/SpiceDBContainer.cs @@ -25,19 +25,4 @@ public string GetGrpcConnectionString() var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(SpiceDBBuilder.SpiceDBPort)); return endpoint.ToString(); } - - public GrpcChannel GetGrpcChannel() - { - return GrpcChannel.ForAddress(GetGrpcConnectionString()); - } - - public async Task GetStateAsync(CancellationToken cancellationToken = default) - { - var healthClient = new Health.HealthClient(GetGrpcChannel()); - var response = await healthClient.CheckAsync(new HealthCheckRequest - { - Service = string.Empty, - }, null, null, cancellationToken); - return response.Status.ToString(); - } } diff --git a/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj b/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj index d91941e50..cdaaeff67 100644 --- a/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj +++ b/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Testcontainers.SpiceDB/Usings.cs b/src/Testcontainers.SpiceDB/Usings.cs index 1ccf064c9..9b95586a0 100644 --- a/src/Testcontainers.SpiceDB/Usings.cs +++ b/src/Testcontainers.SpiceDB/Usings.cs @@ -6,5 +6,4 @@ global using DotNet.Testcontainers.Containers; global using JetBrains.Annotations; global using Grpc.Health.V1; -global using Grpc.Net.Client; global using Grpc.Core; \ No newline at end of file diff --git a/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs index 39076aa49..39c665859 100644 --- a/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs +++ b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs @@ -17,18 +17,6 @@ public ValueTask DisposeAsync() return _spicedbContainer.DisposeAsync(); } - [Fact] - [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task ContainerStartsSuccessfully() - { - // Given - var containerState = await _spicedbContainer.GetStateAsync(TestContext.Current.CancellationToken) - .ConfigureAwait(false); - - // When & Then - Assert.Equal(containerState, "started"); - } - [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] public void ExpectedPortIsMapped() From bb51ca6ed509ab7b9cc4059458c7df0cd85a2dfa Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Fri, 1 Aug 2025 09:01:56 +0200 Subject: [PATCH 6/8] feat(spicedb): update SpiceDB configuration and container to support gRPC and HTTP ports --- Directory.Packages.props | 2 ++ src/Testcontainers.SpiceDB/SpiceDBBuilder.cs | 11 ++++++++--- src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs | 9 ++++++++- src/Testcontainers.SpiceDB/SpiceDBContainer.cs | 2 +- .../SpiceDBContainerTest.cs | 2 +- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3ab6849e8..f2ef1875d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -61,6 +61,8 @@ + + diff --git a/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs b/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs index cc5a672cb..21bae0447 100644 --- a/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs +++ b/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs @@ -6,7 +6,10 @@ public sealed class SpiceDBBuilder : ContainerBuilder /// Initializes a new instance of the class. @@ -42,8 +45,10 @@ protected override SpiceDBBuilder Init() { return base.Init() .WithImage(SpiceDBImage) - .WithPortBinding(SpiceDBPort, true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("SpiceDB-cli", "ping")); + .WithPortBinding(SpiceDBgRPCPort, true) + .WithPortBinding(SpiceDBgHTTPPort, true) + .WithCommand("serve", $"--grpc-preshared-key={DockerResourceConfiguration.GrpcPresharedKey}", $"--datastore-engine={DockerResourceConfiguration.DatastoreEngine}", $"--log-level=info") + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("grpc server started serving")); } /// diff --git a/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs b/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs index 29a92f706..d113b20d4 100644 --- a/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs +++ b/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs @@ -7,13 +7,20 @@ public sealed class SpiceDBConfiguration : ContainerConfiguration /// /// Initializes a new instance of the class. /// - public SpiceDBConfiguration(bool? tslEnabled = false) + public SpiceDBConfiguration(string grpcPresharedKey = "mysecret", string datastoreEngine = "memory", bool? tslEnabled = false) { + GrpcPresharedKey = grpcPresharedKey; + DatastoreEngine = datastoreEngine; TslEnabled = tslEnabled.GetValueOrDefault(false); } public bool TslEnabled { get; set; } + public string GrpcPresharedKey { get; set; } + + public string DatastoreEngine { get; set; } + + /// /// Initializes a new instance of the class. /// diff --git a/src/Testcontainers.SpiceDB/SpiceDBContainer.cs b/src/Testcontainers.SpiceDB/SpiceDBContainer.cs index 56e0a6125..73e0ab107 100644 --- a/src/Testcontainers.SpiceDB/SpiceDBContainer.cs +++ b/src/Testcontainers.SpiceDB/SpiceDBContainer.cs @@ -22,7 +22,7 @@ public SpiceDBContainer(SpiceDBConfiguration configuration) public string GetGrpcConnectionString() { var scheme = _configuration.TslEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; - var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(SpiceDBBuilder.SpiceDBPort)); + var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(SpiceDBBuilder.SpiceDBgRPCPort)); return endpoint.ToString(); } } diff --git a/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs index 39c665859..ac48a63a8 100644 --- a/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs +++ b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs @@ -22,7 +22,7 @@ public ValueTask DisposeAsync() public void ExpectedPortIsMapped() { // Given & When - var mappedPort = _spicedbContainer.GetMappedPublicPort(SpiceDBBuilder.SpiceDBPort); + var mappedPort = _spicedbContainer.GetMappedPublicPort(SpiceDBBuilder.SpiceDBgRPCPort); // Then Assert.True(mappedPort > 0); From c4cbd37360611d456ff2cb005cdf3e704f8b2dd6 Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Fri, 1 Aug 2025 09:07:46 +0200 Subject: [PATCH 7/8] chore(ci): standardize formatting in cicd.yml for branches and paths-ignore --- .github/workflows/cicd.yml | 146 +++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 71 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 687a98b81..c9b96029c 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -2,11 +2,11 @@ name: Continuous Integration & Delivery on: push: - branches: [ develop, main, bugfix/*, feature/* ] - paths-ignore: [ 'docs/**', 'examples/**' ] + branches: [develop, main, bugfix/*, feature/*] + paths-ignore: ["docs/**", "examples/**"] pull_request: - branches: [ develop, main ] - paths-ignore: [ 'docs/**', 'examples/**' ] + branches: [develop, main] + paths-ignore: ["docs/**", "examples/**"] workflow_dispatch: inputs: publish_nuget_package: @@ -31,73 +31,77 @@ jobs: strategy: max-parallel: 6 matrix: - test-projects: [ - { name: "Testcontainers", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Platform.Linux", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Platform.Windows", runs-on: "windows-2022" }, - { name: "Testcontainers.Databases", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.ResourceReaper", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.ActiveMq", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.ArangoDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Azurite", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.BigQuery", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Bigtable", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Cassandra", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.ClickHouse", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.CockroachDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Consul", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.CosmosDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Couchbase", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.CouchDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Db2", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.DynamoDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Elasticsearch", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.EventHubs", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.EventStoreDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.FakeGcsServer", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.FirebirdSql", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Firestore", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.InfluxDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.JanusGraph", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.K3s", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Kafka", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Keycloak", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Kusto", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.LocalStack", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.LowkeyVault", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.MariaDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Milvus", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Minio", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.MongoDb", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.MsSql", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.MySql", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Nats", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Neo4j", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Ollama", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.OpenSearch", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Oracle", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Oracle11", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Oracle18", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Oracle21", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Oracle23", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Papercut", runs-on: "ubuntu-22.04" }, - { 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" }, - { name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.ServiceBus", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Sftp", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Typesense", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.XunitV3", runs-on: "ubuntu-22.04" }, - { name: "Testcontainers.SpiceDb", runs-on: "ubuntu-22.04" } - ] + test-projects: + [ + { name: "Testcontainers", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Platform.Linux", runs-on: "ubuntu-22.04" }, + { + name: "Testcontainers.Platform.Windows", + runs-on: "windows-2022", + }, + { name: "Testcontainers.Databases", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.ResourceReaper", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.ActiveMq", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.ArangoDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Azurite", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.BigQuery", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Bigtable", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Cassandra", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.ClickHouse", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.CockroachDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Consul", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.CosmosDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Couchbase", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.CouchDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Db2", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.DynamoDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Elasticsearch", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.EventHubs", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.EventStoreDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.FakeGcsServer", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.FirebirdSql", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Firestore", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.InfluxDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.JanusGraph", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.K3s", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Kafka", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Keycloak", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Kusto", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.LocalStack", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.LowkeyVault", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.MariaDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Milvus", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Minio", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.MongoDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.MsSql", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.MySql", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Nats", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Neo4j", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Ollama", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.OpenSearch", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Oracle", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Oracle11", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Oracle18", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Oracle21", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Oracle23", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Papercut", runs-on: "ubuntu-22.04" }, + { 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" }, + { name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.ServiceBus", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Sftp", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Typesense", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.XunitV3", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.SpiceDB", runs-on: "ubuntu-22.04" }, + ] runs-on: ${{ matrix.test-projects.runs-on }} From 146403ac91b8789472e4bb4e94af42df1996f59b Mon Sep 17 00:00:00 2001 From: Alejandro Dominguez Borroto Date: Fri, 1 Aug 2025 09:49:32 +0200 Subject: [PATCH 8/8] feat(tests): add gRPC support in SpiceDB tests and update package references --- Directory.Packages.props | 1 + .../SpiceDBContainerTest.cs | 41 ++++++++++++------- .../Testcontainers.SpiceDB.Tests.csproj | 2 + tests/Testcontainers.SpiceDB.Tests/Usings.cs | 2 + 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f2ef1875d..0f6482bfc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs index ac48a63a8..97c825e2e 100644 --- a/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs +++ b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs @@ -1,4 +1,10 @@ +using System; using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Net.Client; +using Xunit; namespace Testcontainers.SpiceDB; @@ -30,10 +36,10 @@ public void ExpectedPortIsMapped() [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task ExecCommandReturnsSuccessful() + public async Task VersionCommandReturnsSuccessful() { // Given - List commands = ["spicedb-cli", "version"]; + List commands = ["spicedb", "version"]; // When var execResult = await _spicedbContainer.ExecAsync(commands, TestContext.Current.CancellationToken) @@ -41,33 +47,40 @@ public async Task ExecCommandReturnsSuccessful() // Then Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); - Assert.Contains("spicedb", execResult.Stdout); } [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PingCommandReturnsSuccessful() + public void GetGrpcConnectionStringNotNull() { - // Given - List commands = ["spicedb-cli", "ping"]; - - // When - var execResult = await _spicedbContainer.ExecAsync(commands, TestContext.Current.CancellationToken) - .ConfigureAwait(true); + // Given & When + var connectionString = _spicedbContainer.GetGrpcConnectionString(); // Then - Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + Assert.NotNull(connectionString); } [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public void GetGrpcConnectionStringReturnsExpectedFormat() + public async Task ShouldConnectToSpiceDB() { - // Given & When + // Given var connectionString = _spicedbContainer.GetGrpcConnectionString(); + var handler = new SocketsHttpHandler(); + handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + + // When + using var channel = GrpcChannel.ForAddress(connectionString, new GrpcChannelOptions + { + HttpHandler = handler + }); // Then - // Note: This test will need to be updated once GetConnectionString() is properly implemented Assert.NotNull(connectionString); + Assert.NotNull(channel); + + // Test connectivity by attempting to connect + await channel.ConnectAsync(); + Assert.Equal(ConnectivityState.Ready, channel.State); } } diff --git a/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj b/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj index 7f8859de5..d1ba0366b 100644 --- a/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj +++ b/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj @@ -6,6 +6,8 @@ Exe + + diff --git a/tests/Testcontainers.SpiceDB.Tests/Usings.cs b/tests/Testcontainers.SpiceDB.Tests/Usings.cs index 89af834f6..21f33a999 100644 --- a/tests/Testcontainers.SpiceDB.Tests/Usings.cs +++ b/tests/Testcontainers.SpiceDB.Tests/Usings.cs @@ -2,3 +2,5 @@ global using DotNet.Testcontainers.Commons; global using Testcontainers.SpiceDB; global using Xunit; +global using Grpc.Core; +global using Grpc.Net.Client;