Skip to main content

Test support Testcontainers IDP

Overview

Contains support for spinning up the ASOL Identity Provider (IDP) as a Docker testcontainer. The package wraps the standard Testcontainers ContainerBuilder pattern with a dedicated fluent IdpBuilder.

The IDP container can automatically seed MongoDB data before the container starts, so OIDC applications and other initial data are available on first request.

This functionality is not yet complete: currently only a service account can be authenticated. Additional authentication scenarios will be supported in the future.

Install

  • ASOL.Core.QA.TestSupport.Testcontainers.Idp

IdpBuilder

IdpBuilder is the entry point for creating an IdpContainer instance. It extends the Testcontainers ContainerBuilder and exposes a fluent API for all IDP configuration concerns.

Construction

// Construct with a full image string
var builder = new IdpBuilder("913524905811.dkr.ecr.eu-central-1.amazonaws.com/ava/asol.identityprovider.api:1.20250915.1");

// Or using the well-known constants
var image = $"{IdpBuilder.DefaultRegistry}/{IdpBuilder.DefaultRepository}:{version}";
var builder = new IdpBuilder(image);
ConstantValue
IdpBuilder.DefaultRegistry913524905811.dkr.ecr.eu-central-1.amazonaws.com
IdpBuilder.DefaultRepositoryava/asol.identityprovider.api

Default configuration (Development environment)

When using IdpBuilder, the following defaults are applied automatically. They can be overridden by calling the corresponding WithXxx methods:

  • ASPNETCORE_ENVIRONMENT = Development
  • ASPNETCORE_URLS = http://+:80;https://+:443 so the container listens on standard ports
    • http: 80
    • https: 443
  • Network alias: idp
  • Self-signed TLS certificate bundled with the package (/certs/idp.pfx, password password)
  • MongoDB: DatabaseName=ASP-Master, CollectionNamePrefix=IDP., HealthCheckMode=SkipAsHealthy
  • RabbitMQ: VirtualHost=/, ClientName=ASOL.IdentityProvider, SSL disabled, HealthCheckMode=SkipAsHealthy
  • Authorization service: ApplicationCode=ASOLEU-IDP-AP-, strict mode = Warning, HealthCheckMode=SkipAsHealthy
  • Reverse proxy: disabled
  • A default OIDC application seed for plaza-services client credentials (with "Password123*" password)

Validation: IdpBuilder.Build() validates that MongoDB connection string, database name, RabbitMQ host name, virtual host, and client name are all set. In non-Development environments additional fields are also validated.

Fluent methods

WithMongoDb

Configures the MongoDB connection for the IDP. Use the overload that accepts a MongoDbContainer (from IdpBuilderExtensions) for automatic container wiring.

// Direct options
builder.WithMongoDb(new MongoDbConnectionOptions(
ConnectionString: "mongodb://admin:admin@database:27017",
DatabaseName: "ASP-Master",
CollectionNamePrefix: "IDP.",
HealthCheckMode: HealthCheckMode.SkipAsHealthy));

// Via IdpBuilderExtensions shortcut (preferred)
builder.WithMongoDb(mongoDbContainer);

See MongoDbConnectionOptions for all properties.

WithRabbitMq / AddRabbitMq

Configures the RabbitMQ connection for the IDP. Use the AddRabbitMq extension (from IdpBuilderExtensions) for automatic container wiring.

// Direct options
builder.WithRabbitMq(new RabbitMqConnectionOptions(
HostName: "eventbus",
Port: 5672,
UserName: "guest",
Password: "guest",
VirtualHost: "/",
ClientName: "ASOL.IdentityProvider",
Ssl: new RabbitMqSslConnectionOptions(Enabled: false)));

// Via IdpBuilderExtensions shortcut (preferred)
builder.AddRabbitMq(rabbitMqContainer);

See RabbitMqConnectionOptions for all properties.

WithAuthorizationService

builder.WithAuthorizationService(new AuthorizationServiceOptions(
BaseUrl: "https://authz-service/",
ApplicationCode: "ASOLEU-IDP-AP-",
ServiceRoleStrictMode: ServiceAuthorizationStrictMode.Warning,
HealthCheckMode: HealthCheckMode.SkipAsHealthy));

See AuthorizationServiceOptions for all properties.

WithReverseProxy

// Disable reverse proxy (default in Development)
builder.WithReverseProxy(new ReverseProxyOptions(Enabled: false));

// Enable with base path trimming
builder.WithReverseProxy(new ReverseProxyOptions(
Enabled: true,
BasePath: "/idp",
UriMode: ReverseProxyUriMode.Trimmed,
ForceHttpsScheme: true));

See ReverseProxyOptions for all properties.

WithIdentityManager

builder.WithIdentityManager(new IdentityManagerClientOptions(
BaseUrl: "https://identity-manager/"));

WithGoogleReCaptcha

builder.WithGoogleReCaptcha(new GoogleReCaptchaOptions(
SiteKey: "my-site-key",
SecretKey: "my-secret-key",
SuppressCaptchaValidatorUrl: "https://captcha-bypass/"));

See GoogleReCaptchaOptions for all properties.

WithTelemetry

builder.WithTelemetry(new TelemetryOptions(
Endpoint: "https://otel-collector:4317/",
ApiVersion: "v1",
InstrumentationKey: "00000000-0000-0000-0000-000000000000",
ServiceName: "ASOL.IdentityProvider",
LogDirectory: "/logs"));

WithMicrosoftExternalProvider

builder.WithMicrosoftExternalProvider(new MicrosoftExternalProviderOptions(
Authority: "https://login.microsoftonline.com/tenant-id/v2.0",
ClientId: "aad-client-id",
ClientSecret: "aad-client-secret",
DisplayName: "Microsoft",
CallbackPath: "/signin-microsoft"));

WithMessageProvider

builder.WithMessageProvider(new MessageProviderClientOptions(
BaseUrl: "https://message-provider/"));

WithSubjectManager

builder.WithSubjectManager(new SubjectManagerClientOptions(
BaseUrl: "https://subject-manager/"));

WithDistributedCache

Configures the IDP distributed cache storage. The ConnectionString value is passed to the shared RedisConnection configuration section used by Redis-backed Core packages. The example uses ASOL.Core.QA.TestSupport.Testcontainers.Idp.Options.DistributedCacheOptions, not ASOL.Core.Caching.Distributed.Options.DistributedCacheOptions. The helper maps ConnectionString to RedisConnection__ConnectionString and keeps Storage in DistributedCache__Storage.

builder.WithDistributedCache(new DistributedCacheOptions(
ConnectionString: "redis:6379",
Storage: "RemoteCache"));

WithCertificateAuthority

builder.WithCertificateAuthority(new CertificateAuthorityOptions(
EncryptionKey: "my-encryption-key"));

WithOidc

builder.WithOidc(new OidcOptions(
CertificatePath: "/certs/oidc.pfx",
DataProtectionFilePersistencePath: "/data-protection-keys",
DataProtectionApplicationCode: "ASOLEU-IDP-AP-",
SigningCertificatePassword: "password",
EncryptionCertificatePassword: "password",
RequireConfirmedAccount: false));

See OidcOptions for all properties.

WithSsoAuth

builder.WithSsoAuth(new SsoAuthOptions(
Authority: "https://idp/",
ClientId: "plaza-services",
ClientSecret: "secret",
RequireHttpsMetadata: false,
Audience: "apiim"));

IdpBuilderExtensions

Convenience shortcuts for wiring up MongoDB and RabbitMQ testcontainers directly to an IdpBuilder, using the IDP's default section names. The containers do not need to be started before calling these methods.

WithMongoDb

Wires MongoDbContainer to the IDP using section name "MasterConnectionOptions".

builder.WithMongoDb(mongoDbContainer);

// With optional overrides
builder.WithMongoDb(mongoDbContainer, new PartialMongoDbConnectionOptions
{
CollectionNamePrefix = "IDP."
});

AddRabbitMq

Wires RabbitMqContainer to the IDP using the default messaging section names ("Messaging" / "RabbitMq").

builder.AddRabbitMq(rabbitMqContainer);

// With optional overrides
builder.AddRabbitMq(rabbitMqContainer, new PartialRabbitMqConnectionOptions
{
ClientName = "ASOL.IdentityProvider"
});

Data Seeding

IdpBuilder can seed MongoDB collections before the container starts. Seeding is performed using a temporary mongosh container that runs against the configured MongoDB instance.

WithDataSeed

Adds a generic seed for any MongoDB collection.

builder.WithDataSeed(new DataSeed(
CollectionName: "MyCollection",
Data: new { Field1 = "value1", Field2 = 42 }));
ParameterDescription
CollectionNameThe MongoDB collection to seed into.
DataThe object to insert (serialized to JSON).

WithOIDCApplicationSeed

Convenience method for seeding an OIDC application into the OIDCApplications collection.

builder.WithOIDCApplicationSeed(new OIDCApplicationSeed(
ApplicationType: "web",
ClientId: "my-client",
ClientSecret: "hashed-secret",
ClientType: "confidential",
ConsentType: "implicit",
DisplayName: "My Application",
DisplayNames: ImmutableDictionary<string, string>.Empty,
Permissions: ["gt:client_credentials", "ept:token", "rst:id_token token"],
PostLogoutRedirectUris: [],
Properties: new object(),
RedirectUris: [],
Requirements: [],
Settings: ImmutableDictionary<string, string>.Empty,
GrantAccessToAllTenants: true,
Released: true,
Deleted: false,
CreatedBy: "test_system",
IntegrationToken: null,
IntegrationTokenSetupCompleted: false,
Description: "My test OIDC application",
IdentityManagementLevel: 1));

The _id and CreatedOn fields are generated automatically by the seeder using MongoDB template tokens.

Examples

Minimal – HTTP / Swagger

public class IdpContainer_Tests : IAsyncLifetime
{
private readonly MongoDbContainer mongoDbContainer;
private readonly RabbitMqContainer rabbitMqContainer;
private readonly RedisContainer redisContainer;
private readonly INetwork network;

public IdpContainer_Tests()
{
network = new NetworkBuilder().Build();

mongoDbContainer = new MongoDbBuilder("mongo:6.0")
.WithNetwork(network)
.WithUsername("admin")
.WithPassword("admin")
.WithNetworkAliases("database")
.Build();

rabbitMqContainer = new RabbitMqBuilder("rabbitmq:3.11")
.WithNetwork(network)
.WithNetworkAliases("eventbus")
.WithUsername("guest")
.WithPassword("guest")
.Build();

redisContainer = new RedisBuilder("redis:8.4")
.WithNetwork(network)
.WithNetworkAliases("redis")
.Build();
}

public ValueTask InitializeAsync() => ValueTask.CompletedTask;

public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
await Task.WhenAll(
mongoDbContainer.DisposeAsync().AsTask(),
rabbitMqContainer.DisposeAsync().AsTask(),
redisContainer.DisposeAsync().AsTask());
}

[Fact]
public async Task Get_Swagger_Success()
{
await using var idpContainer = new IdpBuilder(IdpImageName)
.WithNetwork(network)
.WithMongoDb(mongoDbContainer)
.AddRabbitMq(rabbitMqContainer)
.WithPortBinding(80, assignRandomHostPort: true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(80))
.Build();

await idpContainer.StartAsync(TestContext.Current.CancellationToken);

var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
var port = idpContainer.GetMappedPublicPort(80);
using var httpClient = new HttpClient(handler);

var response = await httpClient.GetAsync(
$"http://localhost:{port}/swagger/index.html",
TestContext.Current.CancellationToken);

Assert.True(response.IsSuccessStatusCode);
}
}

OAuth 2.0 Client Credentials Token

[Fact]
public async Task Get_PlazaServices_AccessToken_Success()
{
await using var idpContainer = new IdpBuilder(IdpImageName)
.WithNetwork(network)
.WithMongoDb(mongoDbContainer)
.AddRabbitMq(rabbitMqContainer)
.WithPortBinding(443, assignRandomHostPort: true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(443))
.Build();

await idpContainer.StartAsync(TestContext.Current.CancellationToken);

var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
var tokenRequest = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", "plaza-services"),
new KeyValuePair<string, string>("client_secret", "Password123*"),
]);
var port = idpContainer.GetMappedPublicPort(443);
using var httpClient = new HttpClient(handler);

var tokenResponse = await httpClient.PostAsync(
$"https://localhost:{port}/connect/token",
tokenRequest,
TestContext.Current.CancellationToken);
var tokenContent = await tokenResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken);

Assert.True(tokenResponse.IsSuccessStatusCode, tokenContent);

using var tokenJson = JsonDocument.Parse(tokenContent);
Assert.True(tokenJson.RootElement.TryGetProperty("access_token", out _));
}