Přeskočit na hlavní obsah

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

Behavior change: no OIDC application is seeded by default any more. Earlier versions implicitly seeded a plaza-services client for every consumer. Call WithOIDCApplicationSeed(clientId, clientSecret) with your own credentials instead — see Data Seeding.

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-core-test",
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 (client credentials)​

Seeds the opinionated test OIDC application for the supplied client credentials. This is the replacement for the OIDC application that earlier versions seeded implicitly.

builder.WithOIDCApplicationSeed("plaza-core-test", IdpBuilder.HashClientSecret("Password123*"));
ParameterDescription
clientIdThe OIDC client identifier to seed. Also used as the display name.
clientSecretThe client secret value stored in the seed. The IDP expects an already hashed secret; a plaintext value seeds a client that can never authenticate.

The seed uses ApplicationType=web, ClientType=confidential, ConsentType=implicit, GrantAccessToAllTenants=true, Released=true, and a broad permission set intended for service-to-service test flows (gt:client_credentials, ept:token, rst:id_token token, gt:login_behalf_of, and the scp:* API scopes).

Use IdpBuilder.CreateOIDCApplicationSeed(clientId, clientSecret) to get the same seed as a record, adjust individual values with a with expression, and pass the result to the overload below.

Both overloads append. Each seed is inserted with insertOne, never upserted, so seeding the same client_id twice creates a second IDP client instead of replacing the first one. Nothing detects it — seeding runs against an empty database before the IDP starts, so no unique index on client_id exists yet.

HashClientSecret​

Hashes a plaintext client secret into the format the IDP stores and verifies.

var plaintextClientSecret = "Password123*";
var hashedClientSecret = IdpBuilder.HashClientSecret(plaintextClientSecret);

Seed hashedClientSecret and send plaintextClientSecret as the client_secret parameter of the token request.

WithOIDCApplicationSeed (full seed)​

Seeds an arbitrary 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​

The OIDC client must be seeded explicitly. The seed stores the hash; the token request sends the plaintext.

private const string PlaintextClientSecret = "Password123*";

[Fact]
public async Task Get_ServiceAccount_AccessToken_Success()
{
await using var idpContainer = new IdpBuilder(IdpImageName)
.WithNetwork(network)
.WithMongoDb(mongoDbContainer)
.AddRabbitMq(rabbitMqContainer)
.WithOIDCApplicationSeed("plaza-core-test", IdpBuilder.HashClientSecret(PlaintextClientSecret))
.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-core-test"),
new KeyValuePair<string, string>("client_secret", PlaintextClientSecret),
]);
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 _));
}