diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index e12f947b2..99186917f 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,72 +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" } - ] + 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 }} diff --git a/Directory.Packages.props b/Directory.Packages.props index 3ab6849e8..0f6482bfc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + @@ -61,6 +62,8 @@ + + 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..c3ea8951d --- /dev/null +++ b/docs/modules/spicedb.md @@ -0,0 +1,56 @@ +# 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. 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 +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(); + + // ... + } + } + ``` + +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" +```` 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..21bae0447 --- /dev/null +++ b/src/Testcontainers.SpiceDB/SpiceDBBuilder.cs @@ -0,0 +1,71 @@ +namespace Testcontainers.SpiceDB; + +/// +[PublicAPI] +public sealed class SpiceDBBuilder : ContainerBuilder +{ + public const string SpiceDBImage = "authzed/spicedb:v1.45.1"; + + public const ushort SpiceDBgRPCPort = 50051; + + public const ushort SpiceDBgHTTPPort = 8443; + + + /// + /// 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(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")); + } + + /// + 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..d113b20d4 --- /dev/null +++ b/src/Testcontainers.SpiceDB/SpiceDBConfiguration.cs @@ -0,0 +1,63 @@ +namespace Testcontainers.SpiceDB; + +/// +[PublicAPI] +public sealed class SpiceDBConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + 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. + /// + /// 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..73e0ab107 --- /dev/null +++ b/src/Testcontainers.SpiceDB/SpiceDBContainer.cs @@ -0,0 +1,28 @@ +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.SpiceDBgRPCPort)); + return endpoint.ToString(); + } +} diff --git a/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj b/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj new file mode 100644 index 000000000..cdaaeff67 --- /dev/null +++ b/src/Testcontainers.SpiceDB/Testcontainers.SpiceDB.csproj @@ -0,0 +1,17 @@ + + + + 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..9b95586a0 --- /dev/null +++ b/src/Testcontainers.SpiceDB/Usings.cs @@ -0,0 +1,9 @@ +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.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..97c825e2e --- /dev/null +++ b/tests/Testcontainers.SpiceDB.Tests/SpiceDBContainerTest.cs @@ -0,0 +1,86 @@ +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; + +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 void ExpectedPortIsMapped() + { + // Given & When + var mappedPort = _spicedbContainer.GetMappedPublicPort(SpiceDBBuilder.SpiceDBgRPCPort); + + // Then + Assert.True(mappedPort > 0); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task VersionCommandReturnsSuccessful() + { + // Given + List commands = ["spicedb", "version"]; + + // 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 GetGrpcConnectionStringNotNull() + { + // Given & When + var connectionString = _spicedbContainer.GetGrpcConnectionString(); + + // Then + Assert.NotNull(connectionString); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ShouldConnectToSpiceDB() + { + // 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 + 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 new file mode 100644 index 000000000..d1ba0366b --- /dev/null +++ b/tests/Testcontainers.SpiceDB.Tests/Testcontainers.SpiceDB.Tests.csproj @@ -0,0 +1,20 @@ + + + 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..21f33a999 --- /dev/null +++ b/tests/Testcontainers.SpiceDB.Tests/Usings.cs @@ -0,0 +1,6 @@ +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using Testcontainers.SpiceDB; +global using Xunit; +global using Grpc.Core; +global using Grpc.Net.Client;