From a068207ee563d11b4ebc6fbd6c4c07f0aa23dd2f Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Sun, 30 Nov 2025 12:02:59 +0100 Subject: [PATCH 01/12] feat: add Zitadel host integration --- CommunityToolkit.Aspire.slnx | 4 + .../AppHost.cs | 8 ++ ...lkit.Aspire.Hosting.Zitadel.AppHost.csproj | 12 ++ .../Properties/launchSettings.json | 31 +++++ .../appsettings.json | 9 ++ ...unityToolkit.Aspire.Hosting.Zitadel.csproj | 8 ++ .../README.md | 34 +++++ .../ZitadelContainerImageTags.cs | 13 ++ .../ZitadelHostingExtensions.cs | 120 ++++++++++++++++++ .../ZitadelResource.cs | 21 +++ 10 files changed, 260 insertions(+) create mode 100644 examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/AppHost.cs create mode 100644 examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj create mode 100644 examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/Properties/launchSettings.json create mode 100644 examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/appsettings.json create mode 100644 src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md create mode 100644 src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index de6a29fef..c1567f3e0 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -179,6 +179,9 @@ + + + @@ -220,6 +223,7 @@ + diff --git a/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/AppHost.cs b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/AppHost.cs new file mode 100644 index 000000000..4f89588cc --- /dev/null +++ b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/AppHost.cs @@ -0,0 +1,8 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var database = builder.AddPostgres("postgres"); + +builder.AddZitadel("zitadel") + .WithDatabase(database); + +builder.Build().Run(); \ No newline at end of file diff --git a/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj new file mode 100644 index 000000000..a13fcb3eb --- /dev/null +++ b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj @@ -0,0 +1,12 @@ + + + + Exe + 981579ce-9426-4ad6-b6f1-192f3f4cf73b + + + + + + + diff --git a/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/Properties/launchSettings.json b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..e0366292f --- /dev/null +++ b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17100;http://localhost:15025", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21182", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23175", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22260" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15025", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19275", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18212", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20294" + } + } + } +} diff --git a/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/appsettings.json b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj b/src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj new file mode 100644 index 000000000..4ba191d5a --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md b/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md new file mode 100644 index 000000000..46b917ace --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md @@ -0,0 +1,34 @@ +# CommunityToolkit.Aspire.Hosting.Zitadel library + +Provides extension methods and resource definitions for a .NET Aspire AppHost to configure Zitadel. + +## Getting Started + +### Install the package + +In your AppHost project, install the package using the following command: + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.Zitadel +``` + +### Example usage + +Then, in the _Program.cs_ file of `AppHost`, define a Zitadel resource, then call `AddZitadel`: + +```csharp +builder.AddZitadel("zitadel"); +``` + +Zitadel requires a Postgres database, you can add one with `AddDatabase`: +```csharp +var database = builder.AddPostgres("postgres"); + +builder.AddZitadel("zitadel") + .AddDatabase(database); +``` +You can also pass in a database rather than server (`AddPostgres().AddDatabase()`). + +## Feedback & contributing + +https://github.com/CommunityToolkit/Aspire diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs new file mode 100644 index 000000000..2026305da --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs @@ -0,0 +1,13 @@ +namespace CommunityToolkit.Aspire.Hosting.Zitadel; + +internal static class ZitadelContainerImageTags +{ + /// Github Container Registry + public const string Registry = "ghcr.io"; + + /// zitadel/zitadel + public const string Image = "zitadel/zitadel"; + + /// v4.7.0 + public const string Tag = "v4.7.0"; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs new file mode 100644 index 000000000..c302522cb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -0,0 +1,120 @@ +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.Zitadel; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Zitadel to an . +/// +public static class ZitadelHostingExtensions +{ + /// + /// Adds a Zitadel container resource to the . + /// + /// + /// + /// + /// + /// + /// + public static IResourceBuilder AddZitadel( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + int? port = null, + IResourceBuilder? password = null, + IResourceBuilder? masterKey = null + ) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minSpecial: 1); + var masterKeyParameter = masterKey?.Resource ?? ParameterResourceBuilderExtensions.CreateGeneratedParameter(builder, $"{name}-masterKey", true, new GenerateParameterDefault + { + MinLength = 32, // Zitadel requires 32, CreateDefaultPasswordParameter generates 22 + Lower = true, + Upper = true, + Numeric = true, + Special = true, + MinLower = 1, + MinUpper = 1, + MinNumeric = 1, + MinSpecial = 1 + }); + + var resource = new ZitadelResource(name) + { + AdminPasswordParameter = passwordParameter + }; + + return builder.AddResource(resource) + .WithImage(ZitadelContainerImageTags.Image) + .WithImageTag(ZitadelContainerImageTags.Tag) + .WithImageRegistry(ZitadelContainerImageTags.Registry) + .WithArgs("start-from-init", "--masterkeyFromEnv") + .WithHttpEndpoint( + targetPort: 8080, + port: port, + name: ZitadelResource.HttpEndpointName + ) + .WithHttpHealthCheck("/healthz") + .WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter) + .WithEnvironment("ZITADEL_TLS_ENABLED", "false") + .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") + .WithEnvironment("ZITADEL_EXTERNALDOMAIN", $"{name}.dev.localhost") + .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard") + .WithEnvironment(static ctx => + { + if (ctx.Resource is ZitadelResource resource) + { + ctx.EnvironmentVariables["ZITADEL_EXTERNALPORT"] = + resource.GetEndpoint(ZitadelResource.HttpEndpointName).Port; + } + }) + // Disable Login V2 for simpler setup (no separate login container needed) + .WithEnvironment("ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED", "false") + // Configure admin user + .WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME", "admin") + .WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD", passwordParameter) + .WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED", "false"); + } + + /// + /// Adds database support to the Zitadel resource. + /// + /// The Zitadel resource to add database support to. + /// The Postgres server resource to use for the database. + /// An optional name for the database Zitadel will use, if left empty will default to "zitadel-db". + public static IResourceBuilder WithDatabase( + this IResourceBuilder builder, + IResourceBuilder server, + [ResourceName] string? databaseName = null + ) + { + databaseName = string.IsNullOrWhiteSpace(databaseName) ? "zitadel-db" : databaseName; + var database = server.AddDatabase(databaseName); + + return WithDatabase(builder, database); + } + + /// + /// Adds database support to the Zitadel resource. + /// + /// The Zitadel resource to add database support to. + /// The Postgres database resource to use for the database. + public static IResourceBuilder WithDatabase(this IResourceBuilder builder, IResourceBuilder database) + { + builder + .WithEnvironment("ZITADEL_DATABASE_POSTGRES_USER_USERNAME", database.Resource.Parent.UserNameReference) + .WithEnvironment("ZITADEL_DATABASE_POSTGRES_USER_PASSWORD", database.Resource.Parent.PasswordParameter) + .WithEnvironment("ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME", database.Resource.Parent.UserNameReference) + .WithEnvironment("ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD", database.Resource.Parent.PasswordParameter) + .WithEnvironment("ZITADEL_DATABASE_POSTGRES_HOST", database.Resource.Parent.Host) + .WithEnvironment("ZITADEL_DATABASE_POSTGRES_PORT", database.Resource.Parent.Port) + .WithEnvironment("ZITADEL_DATABASE_POSTGRES_DATABASE", database.Resource.DatabaseName) + .WithReference(database) + .WaitFor(database); + + return builder; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs new file mode 100644 index 000000000..a27c24870 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs @@ -0,0 +1,21 @@ +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Zitadel; + +/// +/// Resource for the Zitadel API server. +/// +public sealed class ZitadelResource(string name) : ContainerResource(name) +{ + internal const string HttpEndpointName = "http"; + + /// + /// The parameter that contains the (default) Zitadel admin username. + /// + public ParameterResource? AdminUsernameParameter { get; } + + /// + /// The parameter that contains the (default) Zitadel admin password. + /// + public ParameterResource AdminPasswordParameter { get; set; } = null!; // TODO fix null +} \ No newline at end of file From 7f312b3bcb1db9400084dfe53f70ade40bb40a43 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Mon, 1 Dec 2025 20:52:26 +0100 Subject: [PATCH 02/12] feat: add username for Zitadel as well as missing docs --- .../ZitadelHostingExtensions.cs | 16 ++++++++++------ .../ZitadelResource.cs | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index c302522cb..f76e49912 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -11,16 +11,18 @@ public static class ZitadelHostingExtensions /// /// Adds a Zitadel container resource to the . /// - /// - /// - /// - /// - /// + /// The to add the Zitadel container to. + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port used when launching the container. If null a random port will be assigned + /// An optional parameter to set a username for the admin account, if null will auto generate one. + /// An optional parameter to set a password for the admin account, if null will auto generate one. + /// An optional parameter to set the masterkey, if null will auto generate one. /// public static IResourceBuilder AddZitadel( this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null, + IResourceBuilder? username = null, IResourceBuilder? password = null, IResourceBuilder? masterKey = null ) @@ -28,6 +30,7 @@ public static IResourceBuilder AddZitadel( ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(name); + var usernameParameter = username?.Resource ?? new ParameterResource($"{name}-username", _ => "admin", false); var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minSpecial: 1); var masterKeyParameter = masterKey?.Resource ?? ParameterResourceBuilderExtensions.CreateGeneratedParameter(builder, $"{name}-masterKey", true, new GenerateParameterDefault { @@ -44,6 +47,7 @@ public static IResourceBuilder AddZitadel( var resource = new ZitadelResource(name) { + AdminUsernameParameter = usernameParameter, AdminPasswordParameter = passwordParameter }; @@ -74,7 +78,7 @@ public static IResourceBuilder AddZitadel( // Disable Login V2 for simpler setup (no separate login container needed) .WithEnvironment("ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED", "false") // Configure admin user - .WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME", "admin") + .WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME", usernameParameter) .WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD", passwordParameter) .WithEnvironment("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED", "false"); } diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs index a27c24870..3904eb55f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelResource.cs @@ -12,10 +12,10 @@ public sealed class ZitadelResource(string name) : ContainerResource(name) /// /// The parameter that contains the (default) Zitadel admin username. /// - public ParameterResource? AdminUsernameParameter { get; } + public required ParameterResource AdminUsernameParameter { get; set; } /// /// The parameter that contains the (default) Zitadel admin password. /// - public ParameterResource AdminPasswordParameter { get; set; } = null!; // TODO fix null + public required ParameterResource AdminPasswordParameter { get; set; } } \ No newline at end of file From 1130ca30570bffe5da66f28668ad743bcd1eb49a Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Mon, 1 Dec 2025 20:55:33 +0100 Subject: [PATCH 03/12] docs: add NuGet metadata --- .../CommunityToolkit.Aspire.Hosting.Zitadel.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj b/src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj index 4ba191d5a..230ce0999 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.csproj @@ -1,5 +1,10 @@  + + A .NET Aspire host integration for Zitadel. + zitadel auth identity oauth2 authenticatioon openid-connect oidc + + From a7767d90c625e19ec1ecb87a60615d2162fd308e Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Tue, 2 Dec 2025 10:09:38 +0100 Subject: [PATCH 04/12] feat: add missing tests --- .github/workflows/tests.yaml | 1 + CommunityToolkit.Aspire.slnx | 1 + .../ZitadelHostingExtensions.cs | 21 +-- .../AppHostTests.cs | 71 +++++++++ ...oolkit.Aspire.Hosting.Zitadel.Tests.csproj | 10 ++ .../ZitadelHostingExtensionsTests.cs | 130 ++++++++++++++++ .../ZitadelIntegrationTests.cs | 80 ++++++++++ .../ZitadelWithDatabaseTests.cs | 142 ++++++++++++++++++ 8 files changed, 446 insertions(+), 10 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c511bb0d0..b905089ae 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -63,6 +63,7 @@ jobs: Hosting.SqlServer.Extensions.Tests, Hosting.Stripe.Tests, Hosting.SurrealDb.Tests, + Hosting.Zitadel.Tests, # Client integration tests GoFeatureFlag.Tests, diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index c1567f3e0..03a3e46dd 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -282,6 +282,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index f76e49912..4dae8a514 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -51,7 +51,7 @@ public static IResourceBuilder AddZitadel( AdminPasswordParameter = passwordParameter }; - return builder.AddResource(resource) + var zitadelBuilder = builder.AddResource(resource) .WithImage(ZitadelContainerImageTags.Image) .WithImageTag(ZitadelContainerImageTags.Tag) .WithImageRegistry(ZitadelContainerImageTags.Registry) @@ -66,15 +66,14 @@ public static IResourceBuilder AddZitadel( .WithEnvironment("ZITADEL_TLS_ENABLED", "false") .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") .WithEnvironment("ZITADEL_EXTERNALDOMAIN", $"{name}.dev.localhost") - .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard") - .WithEnvironment(static ctx => - { - if (ctx.Resource is ZitadelResource resource) - { - ctx.EnvironmentVariables["ZITADEL_EXTERNALPORT"] = - resource.GetEndpoint(ZitadelResource.HttpEndpointName).Port; - } - }) + .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard"); + + // Use ReferenceExpression for the port to avoid issues with endpoint allocation + var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName); + var portExpression = ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.Port)}"); + + return zitadelBuilder + .WithEnvironment("ZITADEL_EXTERNALPORT", portExpression) // Disable Login V2 for simpler setup (no separate login container needed) .WithEnvironment("ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED", "false") // Configure admin user @@ -108,6 +107,8 @@ public static IResourceBuilder WithDatabase( /// The Postgres database resource to use for the database. public static IResourceBuilder WithDatabase(this IResourceBuilder builder, IResourceBuilder database) { + ArgumentNullException.ThrowIfNull(database); + builder .WithEnvironment("ZITADEL_DATABASE_POSTGRES_USER_USERNAME", database.Resource.Parent.UserNameReference) .WithEnvironment("ZITADEL_DATABASE_POSTGRES_USER_PASSWORD", database.Resource.Parent.PasswordParameter) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs new file mode 100644 index 000000000..025255bc0 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs @@ -0,0 +1,71 @@ +using CommunityToolkit.Aspire.Testing; +using Aspire.Components.Common.Tests; +using System.Net; + +namespace CommunityToolkit.Aspire.Hosting.Zitadel.Tests; + +[RequiresDocker] +public class AppHostTests( + AspireIntegrationTestFixture fixture +) : IClassFixture> +{ + [Fact] + public async Task Zitadel_Starts_And_Responds_Ok() + { + var resourceName = "zitadel"; + + // Wait for Zitadel to be healthy (it has a health check configured) + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(resourceName) + .WaitAsync(TimeSpan.FromMinutes(5)); + + var httpClient = fixture.CreateHttpClient(resourceName); + + // Test the health endpoint + var response = await httpClient.GetAsync("/healthz"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Zitadel_With_Postgres_Starts_And_Is_Healthy() + { + var postgresName = "postgres"; + var zitadelName = "zitadel"; + + // Wait for Postgres to be healthy first + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(postgresName) + .WaitAsync(TimeSpan.FromMinutes(3)); + + // Then wait for Zitadel to be healthy + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(zitadelName) + .WaitAsync(TimeSpan.FromMinutes(5)); + + var httpClient = fixture.CreateHttpClient(zitadelName); + + // Test the health endpoint + var healthResponse = await httpClient.GetAsync("/healthz"); + Assert.Equal(HttpStatusCode.OK, healthResponse.StatusCode); + } + + [Fact] + public async Task Zitadel_Dashboard_Is_Accessible() + { + var resourceName = "zitadel"; + + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(resourceName) + .WaitAsync(TimeSpan.FromMinutes(5)); + + var httpClient = fixture.CreateHttpClient(resourceName); + + // The dashboard should be accessible at the root + var response = await httpClient.GetAsync("/healthz"); + + Assert.True(response.IsSuccessStatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.NotEmpty(content); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests.csproj new file mode 100644 index 000000000..4d44c5fac --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs new file mode 100644 index 000000000..f4ed1fd15 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs @@ -0,0 +1,130 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.Zitadel; + +namespace CommunityToolkit.Aspire.Hosting.Zitadel.Tests; + +public class ZitadelHostingExtensionsTests +{ + [Fact] + public void AddZitadel_Should_Throw_If_Builder_Is_Null() + { + IDistributedApplicationBuilder builder = null!; + + var act = () => builder.AddZitadel("zitadel"); + + var exception = Assert.Throws(act); + Assert.Equal("builder", exception.ParamName); + } + + [Fact] + public void AddZitadel_Should_Throw_If_Name_Is_Null() + { + var builder = DistributedApplication.CreateBuilder(); + + var act = () => builder.AddZitadel(null!); + + var exception = Assert.Throws(act); + Assert.Equal("name", exception.ParamName); + } + + [Fact] + public void AddZitadel_Creates_ZitadelResource() + { + var builder = DistributedApplication.CreateBuilder(); + + var zitadel = builder.AddZitadel("zitadel"); + + Assert.NotNull(zitadel); + Assert.IsType(zitadel.Resource); + Assert.Equal("zitadel", zitadel.Resource.Name); + } + + [Fact] + public async Task AddZitadel_Sets_Default_Environment_Variables() + { + var builder = DistributedApplication.CreateBuilder(); + + var zitadel = builder.AddZitadel("zitadel"); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.Equal("false", env["ZITADEL_TLS_ENABLED"]); + Assert.Equal("false", env["ZITADEL_EXTERNALSECURE"]); + Assert.Equal("zitadel.dev.localhost", env["ZITADEL_EXTERNALDOMAIN"]); + Assert.Equal("false", env["ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED"]); + Assert.Equal("false", env["ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED"]); + } + + [Fact] + public async Task AddZitadel_Sets_Admin_Username_And_Password() + { + var builder = DistributedApplication.CreateBuilder(); + + var zitadel = builder.AddZitadel("zitadel"); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.NotNull(zitadel.Resource.AdminUsernameParameter); + Assert.NotNull(zitadel.Resource.AdminPasswordParameter); + Assert.True(env.ContainsKey("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME")); + Assert.True(env.ContainsKey("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD")); + } + + [Fact] + public void AddZitadel_Uses_Custom_Username_And_Password() + { + var builder = DistributedApplication.CreateBuilder(); + var username = builder.AddParameter("custom-username"); + var password = builder.AddParameter("custom-password"); + + var zitadel = builder.AddZitadel("zitadel", username: username, password: password); + + Assert.Same(username.Resource, zitadel.Resource.AdminUsernameParameter); + Assert.Same(password.Resource, zitadel.Resource.AdminPasswordParameter); + } + + [Fact] + public async Task AddZitadel_Uses_Custom_MasterKey() + { + var builder = DistributedApplication.CreateBuilder(); + var masterKey = builder.AddParameter("custom-masterkey"); + + var zitadel = builder.AddZitadel("zitadel", masterKey: masterKey); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.True(env.ContainsKey("ZITADEL_MASTERKEY")); + } + + [Fact] + public void AddZitadel_Has_HttpEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + var zitadel = builder.AddZitadel("zitadel"); + + var endpoint = zitadel.Resource.Annotations.OfType() + .FirstOrDefault(e => e.Name == "http"); + + Assert.NotNull(endpoint); + } + + [Fact] + public void AddZitadel_With_Custom_Port() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddZitadel("zitadel", port: 8888); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var endpoint = resource.Annotations.OfType() + .First(e => e.Name == "http"); + + Assert.Equal(8888, endpoint.Port); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs new file mode 100644 index 000000000..b5f0db8dc --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs @@ -0,0 +1,80 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Zitadel.Tests; + +[RequiresDocker] +public class ZitadelIntegrationTests( + AspireIntegrationTestFixture fixture +) : IClassFixture> +{ + [Fact] + public async Task Zitadel_WithPostgres_Starts_And_HealthReady_Ok() + { + var postgresName = "postgres"; + var zitadelName = "zitadel"; + + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(postgresName) + .WaitAsync(TimeSpan.FromMinutes(3)); + + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(zitadelName) + .WaitAsync(TimeSpan.FromMinutes(5)); + + var httpClient = fixture.CreateHttpClient(zitadelName); + var response = await httpClient.GetAsync("/healthz"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Zitadel_WithPostgres_Env_Is_Applied_And_DbConfig_Is_Valid() + { + var postgresName = "postgres"; + var zitadelName = "zitadel"; + + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(postgresName) + .WaitAsync(TimeSpan.FromMinutes(3)); + + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(zitadelName) + .WaitAsync(TimeSpan.FromMinutes(5)); + + var appModel = fixture.App.Services.GetRequiredService(); + var zitadelResource = appModel.Resources.OfType() + .Single(r => r.Name == zitadelName); + + var env = await zitadelResource.GetEnvironmentVariableValuesAsync(); + + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_HOST")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_PORT")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_DATABASE")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_USER_USERNAME")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_USER_PASSWORD")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD")); + } + + [Fact] + public async Task Zitadel_Admin_Credentials_Are_Set() + { + var zitadelName = "zitadel"; + + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(zitadelName) + .WaitAsync(TimeSpan.FromMinutes(5)); + + var appModel = fixture.App.Services.GetRequiredService(); + var zitadelResource = appModel.Resources.OfType() + .Single(r => r.Name == zitadelName); + + var env = await zitadelResource.GetEnvironmentVariableValuesAsync(); + + Assert.True(env.ContainsKey("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME")); + Assert.True(env.ContainsKey("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD")); + Assert.NotEmpty(env["ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME"]); + Assert.NotEmpty(env["ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD"]); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs new file mode 100644 index 000000000..59ca30c4d --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs @@ -0,0 +1,142 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Zitadel.Tests; + +public class ZitadelWithDatabaseTests +{ + [Fact] + public void WithDatabase_Should_Throw_If_Builder_Is_Null() + { + IResourceBuilder builder = null!; + var app = DistributedApplication.CreateBuilder(); + var pg = app.AddPostgres("postgres"); + + var act = () => builder.WithDatabase(pg); + + var exception = Assert.Throws(act); + } + + [Fact] + public void WithDatabase_Should_Throw_If_Server_Is_Null() + { + var app = DistributedApplication.CreateBuilder(); + var zitadel = app.AddZitadel("zitadel"); + + var act = () => zitadel.WithDatabase((IResourceBuilder)null!); + + var exception = Assert.Throws(act); + } + + [Fact] + public void WithDatabase_Should_Throw_If_Database_Is_Null() + { + var app = DistributedApplication.CreateBuilder(); + var zitadel = app.AddZitadel("zitadel"); + + var act = () => zitadel.WithDatabase((IResourceBuilder)null!); + + var exception = Assert.Throws(act); + } + + [Fact] + public async Task WithDatabase_Sets_Default_Database_Name() + { + var builder = DistributedApplication.CreateBuilder(); + var pg = builder.AddPostgres("postgres"); + var zitadel = builder.AddZitadel("zitadel") + .WithDatabase(pg); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.Equal("zitadel-db", env["ZITADEL_DATABASE_POSTGRES_DATABASE"]); + } + + [Fact] + public async Task WithDatabase_Uses_Custom_Database_Name() + { + var builder = DistributedApplication.CreateBuilder(); + var pg = builder.AddPostgres("postgres"); + var zitadel = builder.AddZitadel("zitadel") + .WithDatabase(pg, "custom-db"); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.Equal("custom-db", env["ZITADEL_DATABASE_POSTGRES_DATABASE"]); + } + + [Fact] + public async Task WithDatabase_Sets_Postgres_Environment_Variables() + { + var builder = DistributedApplication.CreateBuilder(); + var pg = builder.AddPostgres("postgres"); + var db = pg.AddDatabase("zitadel-db"); + + var zitadel = builder.AddZitadel("zitadel") + .WithDatabase(db); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_USER_USERNAME")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_USER_PASSWORD")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_HOST")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_PORT")); + Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_DATABASE")); + } + + [Fact] + public async Task WithDatabase_Uses_Server_Parameters() + { + var builder = DistributedApplication.CreateBuilder(); + var pg = builder.AddPostgres("postgres"); + var db = pg.AddDatabase("zitadel-db"); + + var zitadel = builder.AddZitadel("zitadel") + .WithDatabase(db); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.NotNull(env["ZITADEL_DATABASE_POSTGRES_USER_USERNAME"]); + Assert.NotNull(env["ZITADEL_DATABASE_POSTGRES_USER_PASSWORD"]); + Assert.NotNull(env["ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME"]); + Assert.NotNull(env["ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD"]); + } + + [Fact] + public void WithDatabase_Creates_WaitFor_Dependency() + { + var app = DistributedApplication.CreateBuilder(); + var pg = app.AddPostgres("postgres"); + var db = pg.AddDatabase("zitadel-db"); + + var zitadel = app.AddZitadel("zitadel") + .WithDatabase(db); + + // The resource should have a WaitFor annotation + var waitForAnnotation = zitadel.Resource.Annotations + .OfType() + .FirstOrDefault(); + + Assert.NotNull(waitForAnnotation); + } + + [Fact] + public void WithDatabase_Creates_Reference() + { + var app = DistributedApplication.CreateBuilder(); + var pg = app.AddPostgres("postgres"); + var db = pg.AddDatabase("zitadel-db"); + + var zitadel = app.AddZitadel("zitadel") + .WithDatabase(db); + + // The resource should have a ResourceReference annotation + var references = zitadel.Resource.Annotations + .OfType() + .ToList(); + + Assert.NotEmpty(references); + } +} From e77b181ce2007788a0f5316cc56624a6a658897d Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Tue, 2 Dec 2025 10:15:30 +0100 Subject: [PATCH 05/12] docs: apply (very) minor doc clean-up --- src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md | 2 +- .../ZitadelHostingExtensions.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md b/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md index 46b917ace..7d2f4a20a 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md @@ -20,7 +20,7 @@ Then, in the _Program.cs_ file of `AppHost`, define a Zitadel resource, then cal builder.AddZitadel("zitadel"); ``` -Zitadel requires a Postgres database, you can add one with `AddDatabase`: +Zitadel *requires* a Postgres database, you can add one with `AddDatabase`: ```csharp var database = builder.AddPostgres("postgres"); diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index 4dae8a514..dc4295740 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -17,7 +17,6 @@ public static class ZitadelHostingExtensions /// An optional parameter to set a username for the admin account, if null will auto generate one. /// An optional parameter to set a password for the admin account, if null will auto generate one. /// An optional parameter to set the masterkey, if null will auto generate one. - /// public static IResourceBuilder AddZitadel( this IDistributedApplicationBuilder builder, [ResourceName] string name, From d76476e78a8e6430306c71c4c726ac0d4de9cd63 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Tue, 2 Dec 2025 14:08:03 +0100 Subject: [PATCH 06/12] feat: allow overriding external domain --- .../README.md | 35 ++++++++- .../ZitadelHostingExtensions.cs | 25 ++++++- .../ZitadelHostingExtensionsTests.cs | 72 +++++++++++++++++++ 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md b/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md index 7d2f4a20a..bf22453f5 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md @@ -20,15 +20,46 @@ Then, in the _Program.cs_ file of `AppHost`, define a Zitadel resource, then cal builder.AddZitadel("zitadel"); ``` -Zitadel *requires* a Postgres database, you can add one with `AddDatabase`: +Zitadel *requires* a Postgres database, you can add one with `WithDatabase`: ```csharp var database = builder.AddPostgres("postgres"); builder.AddZitadel("zitadel") - .AddDatabase(database); + .WithDatabase(database); ``` You can also pass in a database rather than server (`AddPostgres().AddDatabase()`). +### Configuring the External Domain + +By default, Zitadel uses `{name}.dev.localhost` as the external domain, which works well for local development. For production deployments or custom scenarios, you can configure a custom external domain: + +**Option 1: Using the parameter** +```csharp +builder.AddZitadel("zitadel", externalDomain: "auth.example.com"); +``` + +**Option 2: Using the fluent API** +```csharp +builder.AddZitadel("zitadel") + .WithExternalDomain("auth.example.com"); +``` + +**Option 3: From configuration** +```csharp +var domain = builder.Configuration["Zitadel:ExternalDomain"]; +builder.AddZitadel("zitadel", externalDomain: domain); +``` + +#### Why `.dev.localhost`? + +`.dev.localhost` is a special top-level domain that: +- Automatically resolves to `127.0.0.1` without requiring DNS configuration +- Provides unique subdomains for each Zitadel instance (e.g., `zitadel1.dev.localhost`, `zitadel2.dev.localhost`) +- Works reliably in local development and CI/CD environments +- Satisfies Zitadel's requirement for stable hostnames in OIDC/OAuth2 flows + +For production deployments, replace this with your actual domain name using one of the configuration methods above. + ## Feedback & contributing https://github.com/CommunityToolkit/Aspire diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index dc4295740..fba642020 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -17,18 +17,23 @@ public static class ZitadelHostingExtensions /// An optional parameter to set a username for the admin account, if null will auto generate one. /// An optional parameter to set a password for the admin account, if null will auto generate one. /// An optional parameter to set the masterkey, if null will auto generate one. + /// The external domain for Zitadel. Defaults to {name}.dev.localhost which works for local development. For production deployments, specify the actual domain (e.g., "auth.example.com"). public static IResourceBuilder AddZitadel( this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null, IResourceBuilder? username = null, IResourceBuilder? password = null, - IResourceBuilder? masterKey = null + IResourceBuilder? masterKey = null, + string? externalDomain = null ) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(name); + // Use provided external domain or default to {name}.dev.localhost + var domain = externalDomain ?? $"{name}.dev.localhost"; + var usernameParameter = username?.Resource ?? new ParameterResource($"{name}-username", _ => "admin", false); var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minSpecial: 1); var masterKeyParameter = masterKey?.Resource ?? ParameterResourceBuilderExtensions.CreateGeneratedParameter(builder, $"{name}-masterKey", true, new GenerateParameterDefault @@ -64,7 +69,7 @@ public static IResourceBuilder AddZitadel( .WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter) .WithEnvironment("ZITADEL_TLS_ENABLED", "false") .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") - .WithEnvironment("ZITADEL_EXTERNALDOMAIN", $"{name}.dev.localhost") + .WithEnvironment("ZITADEL_EXTERNALDOMAIN", domain) .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard"); // Use ReferenceExpression for the port to avoid issues with endpoint allocation @@ -121,4 +126,20 @@ public static IResourceBuilder WithDatabase(this IResourceBuild return builder; } + + /// + /// Configures the external domain for the Zitadel resource. This overrides the default domain set in . + /// + /// The Zitadel resource builder. + /// The external domain to use (e.g., "auth.example.com"). Cannot be null or empty. + /// The resource builder for chaining. + /// Thrown if is null or whitespace. + public static IResourceBuilder WithExternalDomain( + this IResourceBuilder builder, + string externalDomain) + { + ArgumentException.ThrowIfNullOrWhiteSpace(externalDomain); + + return builder.WithEnvironment("ZITADEL_EXTERNALDOMAIN", externalDomain); + } } \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs index f4ed1fd15..c41337c95 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs @@ -127,4 +127,76 @@ public void AddZitadel_With_Custom_Port() Assert.Equal(8888, endpoint.Port); } + + [Fact] + public async Task AddZitadel_Uses_Custom_ExternalDomain() + { + var builder = DistributedApplication.CreateBuilder(); + + var zitadel = builder.AddZitadel("zitadel", externalDomain: "auth.example.com"); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.Equal("auth.example.com", env["ZITADEL_EXTERNALDOMAIN"]); + } + + [Fact] + public async Task WithExternalDomain_Overrides_Default() + { + var builder = DistributedApplication.CreateBuilder(); + + var zitadel = builder.AddZitadel("zitadel") + .WithExternalDomain("custom.domain.com"); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.Equal("custom.domain.com", env["ZITADEL_EXTERNALDOMAIN"]); + } + + [Fact] + public async Task WithExternalDomain_Can_Override_Parameter() + { + var builder = DistributedApplication.CreateBuilder(); + + var zitadel = builder.AddZitadel("zitadel", externalDomain: "first.example.com") + .WithExternalDomain("second.example.com"); + + var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + + // WithExternalDomain should override the parameter + Assert.Equal("second.example.com", env["ZITADEL_EXTERNALDOMAIN"]); + } + + [Fact] + public void WithExternalDomain_Throws_If_Null() + { + var builder = DistributedApplication.CreateBuilder(); + var zitadel = builder.AddZitadel("zitadel"); + + var act = () => zitadel.WithExternalDomain(null!); + + Assert.Throws(act); + } + + [Fact] + public void WithExternalDomain_Throws_If_Empty() + { + var builder = DistributedApplication.CreateBuilder(); + var zitadel = builder.AddZitadel("zitadel"); + + var act = () => zitadel.WithExternalDomain(""); + + Assert.Throws(act); + } + + [Fact] + public void WithExternalDomain_Throws_If_Whitespace() + { + var builder = DistributedApplication.CreateBuilder(); + var zitadel = builder.AddZitadel("zitadel"); + + var act = () => zitadel.WithExternalDomain(" "); + + Assert.Throws(act); + } } From cb6de43982576205f884ba5d4779bd8d1f84745f Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Wed, 3 Dec 2025 09:28:07 +0100 Subject: [PATCH 07/12] test: don't call health check for Zitadel integration test --- .../AppHostTests.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs index 025255bc0..e77e178a5 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs @@ -1,6 +1,7 @@ using CommunityToolkit.Aspire.Testing; using Aspire.Components.Common.Tests; using System.Net; +using System.Net.Http.Json; namespace CommunityToolkit.Aspire.Hosting.Zitadel.Tests; @@ -22,9 +23,36 @@ await fixture.ResourceNotificationService var httpClient = fixture.CreateHttpClient(resourceName); // Test the health endpoint - var response = await httpClient.GetAsync("/healthz"); + var request = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openid-configuration"); + // Needs to match the external domain for Zitadel or we get a 404 + request.Headers.Host = $"{resourceName}.dev.localhost"; + var response = await httpClient.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Zitadel_Starts_And_Serves_Dashboard() + { + var resourceName = "zitadel"; + + // Wait for Zitadel to be healthy (it has a health check configured) + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(resourceName) + .WaitAsync(TimeSpan.FromMinutes(5)); + + var httpClient = fixture.CreateHttpClient(resourceName); + + // Test the health endpoint + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + // Needs to match the external domain for Zitadel or we get a 404 + request.Headers.Host = $"{resourceName}.dev.localhost"; + var response = await httpClient.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains(" Date: Mon, 8 Dec 2025 13:16:24 +0100 Subject: [PATCH 08/12] refactor: use endpoint for `EXTERNAL_DOMAIN` in Zitadel --- .../ZitadelHostingExtensions.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index fba642020..aef826efe 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -17,23 +17,18 @@ public static class ZitadelHostingExtensions /// An optional parameter to set a username for the admin account, if null will auto generate one. /// An optional parameter to set a password for the admin account, if null will auto generate one. /// An optional parameter to set the masterkey, if null will auto generate one. - /// The external domain for Zitadel. Defaults to {name}.dev.localhost which works for local development. For production deployments, specify the actual domain (e.g., "auth.example.com"). public static IResourceBuilder AddZitadel( this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null, IResourceBuilder? username = null, IResourceBuilder? password = null, - IResourceBuilder? masterKey = null, - string? externalDomain = null + IResourceBuilder? masterKey = null ) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(name); - // Use provided external domain or default to {name}.dev.localhost - var domain = externalDomain ?? $"{name}.dev.localhost"; - var usernameParameter = username?.Resource ?? new ParameterResource($"{name}-username", _ => "admin", false); var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minSpecial: 1); var masterKeyParameter = masterKey?.Resource ?? ParameterResourceBuilderExtensions.CreateGeneratedParameter(builder, $"{name}-masterKey", true, new GenerateParameterDefault @@ -69,14 +64,15 @@ public static IResourceBuilder AddZitadel( .WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter) .WithEnvironment("ZITADEL_TLS_ENABLED", "false") .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") - .WithEnvironment("ZITADEL_EXTERNALDOMAIN", domain) .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard"); // Use ReferenceExpression for the port to avoid issues with endpoint allocation var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName); var portExpression = ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.Port)}"); + var hostExpression = ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.Host)}"); return zitadelBuilder + .WithEnvironment("ZITADEL_EXTERNALDOMAIN", hostExpression) .WithEnvironment("ZITADEL_EXTERNALPORT", portExpression) // Disable Login V2 for simpler setup (no separate login container needed) .WithEnvironment("ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED", "false") From 83c87a660b729bc44da3dc0542fde5c8f92f18a3 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Wed, 10 Dec 2025 12:42:52 +0100 Subject: [PATCH 09/12] feat: added HTTPS support for Zitadel --- .../ZitadelHostingExtensions.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index aef826efe..132500d5d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -1,5 +1,6 @@ using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.Zitadel; +using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting; @@ -64,13 +65,54 @@ public static IResourceBuilder AddZitadel( .WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter) .WithEnvironment("ZITADEL_TLS_ENABLED", "false") .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") + .WithHttpsCertificateConfiguration(ctx => + { + ctx.EnvironmentVariables["ZITADEL_EXTERNALSECURE"] = "true"; + ctx.EnvironmentVariables["ZITADEL_TLS_ENABLED"] = "true"; + ctx.EnvironmentVariables["ZITADEL_TLS_CERTPATH"] = ctx.CertificatePath; + ctx.EnvironmentVariables["ZITADEL_TLS_KEYPATH"] = ctx.KeyPath; + return Task.CompletedTask; + }) .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard"); // Use ReferenceExpression for the port to avoid issues with endpoint allocation - var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName); + var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName, KnownNetworkIdentifiers.LocalhostNetwork); var portExpression = ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.Port)}"); var hostExpression = ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.Host)}"); + if (builder.ExecutionContext.IsRunMode) + { + builder.Eventing.Subscribe((@event, cancellationToken) => + { + var developerCertificateService = @event.Services.GetRequiredService(); + + bool addHttps = false; + if (!zitadelBuilder.Resource.TryGetLastAnnotation(out var annotation)) + { + if (developerCertificateService.UseForHttps) + { + // If no certificate is configured, and the developer certificate service supports container trust, + // configure the resource to use the developer certificate for its key pair. + addHttps = true; + } + } + else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForHttps) || annotation.Certificate is not null) + { + addHttps = true; + } + + if (addHttps) + { + // If a TLS certificate is configured, override the endpoint to use HTTPS instead of HTTP + // Zitadel only binds to a single port + zitadelBuilder + .WithEndpoint(ZitadelResource.HttpEndpointName, ep => ep.UriScheme = "https"); + } + + return Task.CompletedTask; + }); + } + return zitadelBuilder .WithEnvironment("ZITADEL_EXTERNALDOMAIN", hostExpression) .WithEnvironment("ZITADEL_EXTERNALPORT", portExpression) From 31cdccdd8bec63d890a3905a16f7d61f53f53468 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Fri, 19 Dec 2025 10:29:49 +0100 Subject: [PATCH 10/12] feat: suppress compiler warnings --- .../ZitadelHostingExtensions.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index 132500d5d..7159177f7 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -65,16 +65,19 @@ public static IResourceBuilder AddZitadel( .WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter) .WithEnvironment("ZITADEL_TLS_ENABLED", "false") .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") - .WithHttpsCertificateConfiguration(ctx => - { - ctx.EnvironmentVariables["ZITADEL_EXTERNALSECURE"] = "true"; - ctx.EnvironmentVariables["ZITADEL_TLS_ENABLED"] = "true"; - ctx.EnvironmentVariables["ZITADEL_TLS_CERTPATH"] = ctx.CertificatePath; - ctx.EnvironmentVariables["ZITADEL_TLS_KEYPATH"] = ctx.KeyPath; - return Task.CompletedTask; - }) .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard"); +#pragma warning disable ASPIRECERTIFICATES001 Allow for Zitadel SSL support + zitadelBuilder.WithHttpsCertificateConfiguration(ctx => + { + ctx.EnvironmentVariables["ZITADEL_EXTERNALSECURE"] = "true"; + ctx.EnvironmentVariables["ZITADEL_TLS_ENABLED"] = "true"; + ctx.EnvironmentVariables["ZITADEL_TLS_CERTPATH"] = ctx.CertificatePath; + ctx.EnvironmentVariables["ZITADEL_TLS_KEYPATH"] = ctx.KeyPath; + return Task.CompletedTask; + }); +#pragma warning restore ASPIRECERTIFICATES001 + // Use ReferenceExpression for the port to avoid issues with endpoint allocation var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName, KnownNetworkIdentifiers.LocalhostNetwork); var portExpression = ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.Port)}"); @@ -82,6 +85,7 @@ public static IResourceBuilder AddZitadel( if (builder.ExecutionContext.IsRunMode) { +#pragma warning disable ASPIRECERTIFICATES001 Allow for Zitadel SSL support builder.Eventing.Subscribe((@event, cancellationToken) => { var developerCertificateService = @event.Services.GetRequiredService(); @@ -111,6 +115,7 @@ public static IResourceBuilder AddZitadel( return Task.CompletedTask; }); +#pragma warning restore ASPIRECERTIFICATES001 } return zitadelBuilder From d4855cb30d63c79aabca78ec42912701520e9f68 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Mon, 12 Jan 2026 13:20:25 +0100 Subject: [PATCH 11/12] fix: update Aspire.AppHost.Sdk version --- .../CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj | 2 +- .../ZitadelHostingExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj index a13fcb3eb..d00f172cb 100644 --- a/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj +++ b/examples/zitadel/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost/CommunityToolkit.Aspire.Hosting.Zitadel.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index 7159177f7..ef5018142 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -67,7 +67,7 @@ public static IResourceBuilder AddZitadel( .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard"); -#pragma warning disable ASPIRECERTIFICATES001 Allow for Zitadel SSL support +#pragma warning disable ASPIRECERTIFICATES001 zitadelBuilder.WithHttpsCertificateConfiguration(ctx => { ctx.EnvironmentVariables["ZITADEL_EXTERNALSECURE"] = "true"; @@ -85,7 +85,7 @@ public static IResourceBuilder AddZitadel( if (builder.ExecutionContext.IsRunMode) { -#pragma warning disable ASPIRECERTIFICATES001 Allow for Zitadel SSL support +#pragma warning disable ASPIRECERTIFICATES001 builder.Eventing.Subscribe((@event, cancellationToken) => { var developerCertificateService = @event.Services.GetRequiredService(); From 31e39e8b281d7fd41ffad5c6472d321e8a76f2d8 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Tue, 13 Jan 2026 07:03:12 +0100 Subject: [PATCH 12/12] fix: update tests with API changes --- .../ZitadelContainerImageTags.cs | 4 +-- .../ZitadelHostingExtensions.cs | 2 -- .../AppHostTests.cs | 4 +-- .../ZitadelHostingExtensionsTests.cs | 25 ++++++------------- .../ZitadelIntegrationTests.cs | 4 +-- .../ZitadelWithDatabaseTests.cs | 9 ++++--- 6 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs index 2026305da..bc9984f82 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelContainerImageTags.cs @@ -8,6 +8,6 @@ internal static class ZitadelContainerImageTags /// zitadel/zitadel public const string Image = "zitadel/zitadel"; - /// v4.7.0 - public const string Tag = "v4.7.0"; + /// v4.9.0 + public const string Tag = "v4.9.0"; } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs index ef5018142..2c1847d68 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs @@ -63,8 +63,6 @@ public static IResourceBuilder AddZitadel( ) .WithHttpHealthCheck("/healthz") .WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter) - .WithEnvironment("ZITADEL_TLS_ENABLED", "false") - .WithEnvironment("ZITADEL_EXTERNALSECURE", "false") .WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard"); #pragma warning disable ASPIRECERTIFICATES001 diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs index e77e178a5..e79b408e8 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/AppHostTests.cs @@ -25,7 +25,7 @@ await fixture.ResourceNotificationService // Test the health endpoint var request = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openid-configuration"); // Needs to match the external domain for Zitadel or we get a 404 - request.Headers.Host = $"{resourceName}.dev.localhost"; + request.Headers.Host = $"{fixture.App.GetEndpoint(resourceName, "http").Host}"; var response = await httpClient.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -46,7 +46,7 @@ await fixture.ResourceNotificationService // Test the health endpoint var request = new HttpRequestMessage(HttpMethod.Get, "/"); // Needs to match the external domain for Zitadel or we get a 404 - request.Headers.Host = $"{resourceName}.dev.localhost"; + request.Headers.Host = $"{fixture.App.GetEndpoint(resourceName, "http").Host}"; var response = await httpClient.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs index c41337c95..7ad277cbe 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs @@ -1,6 +1,7 @@ using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.Zitadel; +using CommunityToolkit.Aspire.Testing; namespace CommunityToolkit.Aspire.Hosting.Zitadel.Tests; @@ -47,7 +48,7 @@ public async Task AddZitadel_Sets_Default_Environment_Variables() var zitadel = builder.AddZitadel("zitadel"); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.Equal("false", env["ZITADEL_TLS_ENABLED"]); Assert.Equal("false", env["ZITADEL_EXTERNALSECURE"]); @@ -63,7 +64,7 @@ public async Task AddZitadel_Sets_Admin_Username_And_Password() var zitadel = builder.AddZitadel("zitadel"); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.NotNull(zitadel.Resource.AdminUsernameParameter); Assert.NotNull(zitadel.Resource.AdminPasswordParameter); @@ -92,7 +93,7 @@ public async Task AddZitadel_Uses_Custom_MasterKey() var zitadel = builder.AddZitadel("zitadel", masterKey: masterKey); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.True(env.ContainsKey("ZITADEL_MASTERKEY")); } @@ -128,18 +129,6 @@ public void AddZitadel_With_Custom_Port() Assert.Equal(8888, endpoint.Port); } - [Fact] - public async Task AddZitadel_Uses_Custom_ExternalDomain() - { - var builder = DistributedApplication.CreateBuilder(); - - var zitadel = builder.AddZitadel("zitadel", externalDomain: "auth.example.com"); - - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); - - Assert.Equal("auth.example.com", env["ZITADEL_EXTERNALDOMAIN"]); - } - [Fact] public async Task WithExternalDomain_Overrides_Default() { @@ -148,7 +137,7 @@ public async Task WithExternalDomain_Overrides_Default() var zitadel = builder.AddZitadel("zitadel") .WithExternalDomain("custom.domain.com"); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.Equal("custom.domain.com", env["ZITADEL_EXTERNALDOMAIN"]); } @@ -158,10 +147,10 @@ public async Task WithExternalDomain_Can_Override_Parameter() { var builder = DistributedApplication.CreateBuilder(); - var zitadel = builder.AddZitadel("zitadel", externalDomain: "first.example.com") + var zitadel = builder.AddZitadel("zitadel") .WithExternalDomain("second.example.com"); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); // WithExternalDomain should override the parameter Assert.Equal("second.example.com", env["ZITADEL_EXTERNALDOMAIN"]); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs index b5f0db8dc..135a6bc4f 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelIntegrationTests.cs @@ -46,7 +46,7 @@ await fixture.ResourceNotificationService var zitadelResource = appModel.Resources.OfType() .Single(r => r.Name == zitadelName); - var env = await zitadelResource.GetEnvironmentVariableValuesAsync(); + var env = await zitadelResource.GetEnvironmentVariablesAsync(); Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_HOST")); Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_PORT")); @@ -70,7 +70,7 @@ await fixture.ResourceNotificationService var zitadelResource = appModel.Resources.OfType() .Single(r => r.Name == zitadelName); - var env = await zitadelResource.GetEnvironmentVariableValuesAsync(); + var env = await zitadelResource.GetEnvironmentVariablesAsync(); Assert.True(env.ContainsKey("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME")); Assert.True(env.ContainsKey("ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD")); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs index 59ca30c4d..c0123c754 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelWithDatabaseTests.cs @@ -1,5 +1,6 @@ using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Testing; namespace CommunityToolkit.Aspire.Hosting.Zitadel.Tests; @@ -47,7 +48,7 @@ public async Task WithDatabase_Sets_Default_Database_Name() var zitadel = builder.AddZitadel("zitadel") .WithDatabase(pg); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.Equal("zitadel-db", env["ZITADEL_DATABASE_POSTGRES_DATABASE"]); } @@ -60,7 +61,7 @@ public async Task WithDatabase_Uses_Custom_Database_Name() var zitadel = builder.AddZitadel("zitadel") .WithDatabase(pg, "custom-db"); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.Equal("custom-db", env["ZITADEL_DATABASE_POSTGRES_DATABASE"]); } @@ -75,7 +76,7 @@ public async Task WithDatabase_Sets_Postgres_Environment_Variables() var zitadel = builder.AddZitadel("zitadel") .WithDatabase(db); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_USER_USERNAME")); Assert.True(env.ContainsKey("ZITADEL_DATABASE_POSTGRES_USER_PASSWORD")); @@ -96,7 +97,7 @@ public async Task WithDatabase_Uses_Server_Parameters() var zitadel = builder.AddZitadel("zitadel") .WithDatabase(db); - var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync(); + var env = await zitadel.Resource.GetEnvironmentVariablesAsync(); Assert.NotNull(env["ZITADEL_DATABASE_POSTGRES_USER_USERNAME"]); Assert.NotNull(env["ZITADEL_DATABASE_POSTGRES_USER_PASSWORD"]);