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);
| Constant | Value |
|---|---|
IdpBuilder.DefaultRegistry | 913524905811.dkr.ecr.eu-central-1.amazonaws.com |
IdpBuilder.DefaultRepository | ava/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=DevelopmentASPNETCORE_URLS=http://+:80;https://+:443so the container listens on standard ports- http: 80
- https: 443
- Network alias:
idp - Self-signed TLS certificate bundled with the package (
/certs/idp.pfx, passwordpassword) - 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-servicesclient 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-Developmentenvironments 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 }));
| Parameter | Description |
|---|---|
CollectionName | The MongoDB collection to seed into. |
Data | The 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 _));
}