Test support Testcontainers Platform
Overview
ASOL.Core.QA.TestSupport.Testcontainers.Platform is the application-level orchestration package for integration-test topologies. It starts shared infrastructure, resolves service images, creates a private container network, starts application services, and exposes host-side runtime information.
Use the package when an integration test needs several AVA services or infrastructure containers to run as one topology. The lower-level ASOL.Core.QA.TestSupport.Testcontainers.* packages build individual containers and use a different lifecycle model. They are not direct replacements for Platform services.
Package selection
ASOL.Core.QA.TestSupport.Testcontainers.Platform- shared MongoDB, RabbitMQ, Redis, image resolution, generic application services, runtime metadata, andTestContainersWebApplicationFactory<TStartup>.ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp- optional Platform-native local Identity Provider service with generated client credentials and HTTP-only startup.ASOL.Core.QA.TestSupport.ApiService- host-sideLocalIdpClientTokenProviderandUseLocalIdpClientTokenProvider()for service-token acquisition from the local IDP.ASOL.Core.QA.TestSupport.Testcontainers.Idp- lower-level standalone IDP Testcontainers builder. Do not combine it with Platform as a replacement forPlatform.Idp.
The published package names use Testcontainers, matching the .NET library name. The configuration section remains TestContainers for backward compatibility.
Minimal configuration
Keep non-sensitive topology defaults in the integration-test appsettings.json:
{
"TestContainers": {
"ImagesPath": "./images",
"DumpsPath": "./dumps",
"MongoImage": "mongo:7.0.24",
"MongoSeederImage": "mongo:7.0.24",
"RabbitMqImage": "rabbitmq:3.13-management",
"RedisImage": "redis:7",
"RabbitMqUsername": "guest",
"RabbitMqPassword": "guest",
"MongoRepositoryUri": "913524905811.dkr.ecr.eu-central-1.amazonaws.com/ava/asol-testingdataprovider",
"IdentityProviderRepositoryUri": "913524905811.dkr.ecr.eu-central-1.amazonaws.com/ava/asol-identityprovider-be:latest-develop",
"SubjectManagerRepositoryUri": "913524905811.dkr.ecr.eu-central-1.amazonaws.com/ava/asol.subjectmanager.api:latest-release",
"PlatformStoreStoreRepositoryUri": "913524905811.dkr.ecr.eu-central-1.amazonaws.com/ava/asol.platformstore.store.api",
"AwsEcr": {
"Region": "eu-central-1"
}
}
}
Use user secrets or environment variables for machine-specific ECR credentials:
{
"TestContainers": {
"AwsEcr": {
"AccessKeyId": "",
"SecretAccessKey": "",
"SessionToken": ""
}
}
}
The corresponding environment variables are TestContainers__AwsEcr__AccessKeyId, TestContainers__AwsEcr__SecretAccessKey, and the optional TestContainers__AwsEcr__SessionToken. Do not keep application runtime credentials, remote alpha/beta authentication values, or copied local Mongo/Rabbit connection strings in user secrets. Platform generates or overrides those values for the current test run.
Platform setup
Derive the application test factory from TestContainersWebApplicationFactory<TStartup> and build the topology in CreateBuilderAsync:
using ASOL.Core.QA.TestSupport.Testcontainers.Platform;
using ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp;
using Microsoft.Extensions.Configuration;
protected override TestPlatformBuilder CreateBuilderAsync(CancellationToken ct = default)
{
static string Required(IConfiguration configuration, string key)
=> configuration[key]
?? throw new InvalidOperationException($"Required configuration '{key}' is missing.");
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.test.json", optional: true)
.AddUserSecrets<ConsumerTestAssemblyMarker>(optional: true)
.AddEnvironmentVariables()
.Build();
var builder = new TestPlatformBuilder(configuration)
.UseRestoredMongoImage();
var localIdp = builder
.AddLocalIdp(new LocalIdpOptions("plaza-services")
{
Audience = "apiim",
MongoSeederImage = Required(configuration, "TestContainers:MongoSeederImage")
})
.WithAwsRepository(Required(configuration, "TestContainers:IdentityProviderRepositoryUri"));
var credentials = localIdp.Credentials;
return localIdp.Done()
.AddService(
archiveFileName: "subject-manager",
configSection: "SubjectManagerClientOptions",
needsMongo: true,
internalPort: 5010,
health: "/health",
configure: service => service.WithCustomConfiguration(new Dictionary<string, string>
{
["SsoAuth__Authority"] = credentials.InternalAuthority,
["SsoAuth__RequireHttpsMetadata"] = "false",
["SsoAuth__Audience"] = "apiim",
["SsoAuth__ClientId"] = credentials.ClientId,
["SsoAuth__ClientSecret"] = credentials.ClientSecret
}))
.WithAwsRepository(Required(configuration, "TestContainers:SubjectManagerRepositoryUri"))
.AddService(
archiveFileName: "store",
configSection: "PlatformStoreStoreClientOptions",
needsMongo: true,
internalPort: 5007,
health: "/health",
configure: service => service.WithCustomConfiguration(new Dictionary<string, string>
{
["SsoAuth__Authority"] = credentials.InternalAuthority,
["SsoAuth__RequireHttpsMetadata"] = "false",
["SsoAuth__Audience"] = "apiim",
["SsoAuth__ClientId"] = credentials.ClientId,
["SsoAuth__ClientSecret"] = credentials.ClientSecret
}))
.WithAwsRepository(Required(configuration, "TestContainers:PlatformStoreStoreRepositoryUri"));
}
The configure callback supplies authentication values that are specific to the current local IDP test run. InternalAuthority is the IDP address reachable from other containers on the Platform network, HTTPS metadata is disabled because the local IDP runs over HTTP, and the audience must match LocalIdpOptions.Audience. The client ID and secret are the same generated credentials that Platform writes to the MongoDB OIDC seed before starting the IDP.
The dictionary in the example contains only the local IDP authentication overrides. A consuming application normally combines these values with its existing service-specific environment configuration before passing the result to WithCustomConfiguration(). Applications can keep repeated AddService() registrations concise by performing that merge in their existing shared configuration helper. Apply these overrides to every containerized service that authenticates against the local IDP.
The example registers Subject Manager and Store in one fluent chain. Each WithAwsRepository() call configures the service added by the immediately preceding AddService() call. Continue the same pattern for the remaining services in the test topology.
ConsumerTestAssemblyMarker is any type declared in the consuming integration-test assembly. AddLocalIdp automatically requires Platform MongoDB and RabbitMQ. It generates a random per-run client secret, verifies that exactly one matching OIDC seed exists, updates the seed before IDP startup, and starts the IDP over HTTP without a Kestrel PFX mapping.
The configured ClientId must exist exactly once in the restored MongoDB seed. Startup fails when the client is missing or duplicated. MongoSeederImage is pinned because it is a real temporary container used by the production starter, not only a test helper.
Local IDP runtime information
After Platform initialization, use GetLocalIdpRuntime() to configure the in-process test application:
var localIdp = Platform.GetLocalIdpRuntime();
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["SsoAuth:Authority"] = localIdp.HostAuthority,
["SsoAuth:RequireHttpsMetadata"] = "false",
["SsoAuth:ClientId"] = localIdp.ClientId,
["SsoAuth:ClientSecret"] = localIdp.ClientSecret,
["IdentityProviderClientOptions:BaseUrl"] = localIdp.HostAuthority
});
The runtime information contains:
HostAuthority- mapped loopback URL used by the test host.InternalAuthority- network alias URL used by service containers.Audience- service-token scope and audience configured for the local IDP.ClientIdandClientSecret- credentials for the current test run.ValidIssuers- normalized internal and host issuer values for JWT validation.
Use InternalAuthority in service container environment values. Use HostAuthority from code running on the developer machine or build agent.
Host-side JWT bearer validation
The local IDP discovery document can advertise absolute endpoints that use its internal or HTTPS authority. Configure the test application's JWT bearer handler with UseLocalIdp() so host-side discovery and JWKS requests are routed through the mapped HostAuthority:
using ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp;
using Microsoft.AspNetCore.Authentication.JwtBearer;
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => options.UseLocalIdp(localIdp));
For an existing or named registration, apply the same options extension to that scheme:
services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme,
options => options.UseLocalIdp(localIdp));
Apply UseLocalIdp() during the regular options configuration phase. Do not call it from PostConfigure: the JWT bearer framework may already have created its ConfigurationManager with the previous backchannel, so later backchannel changes would not affect discovery and JWKS retrieval.
The extension disables HTTPS metadata for the HTTP-only test IDP, applies the runtime Audience, preserves the existing TokenValidationParameters, adds the runtime audience and all runtime ValidIssuers, and keeps raw JWT claim names such as tid. Its non-static backchannel changes only the authority of absolute metadata endpoints; their path and query remain unchanged. This does not change token production: issued tokens continue to use InternalAuthority as their issuer.
Host-side token acquisition
The regular ClientTokenProvider loads OIDC discovery metadata. Local container topologies may publish an internal issuer while the test host reaches the IDP through a mapped port. LocalIdpClientTokenProvider avoids that mismatch by posting directly to {Authority}/connect/token.
Register it in the test startup after the standard API token providers:
services.AddStandardApiTokenProviders();
services.UseLocalIdpClientTokenProvider();
Configure SsoAuthOptions.Authority with HostAuthority, then configure JWT bearer authentication through JwtBearerOptions.UseLocalIdp(). The provider preserves Audience as the requested scope and adds tid when a valid tenant context is present. UseLocalIdpClientTokenProvider() creates a dedicated named host-side HttpClient; token acquisition never reuses the native client of an application connector or WebApplicationFactory.
Generic services and specialized starters
Use AddService for regular application services. A specialized package can register an IServiceContainerStarter through WithServiceContainerStarter when it needs custom startup while retaining Platform image resolution, environment contributors, network, and lifecycle.
Exactly one specialized starter may match a service registration. Platform throws InvalidOperationException when multiple starters match, instead of choosing by registration order.
Specialized starters can publish string metadata through ServiceContainerStartResult. The metadata is available from the corresponding ServiceRuntimeInfo.Metadata entry.
Image resolution behavior
WithAwsRepository accepts a repository URI or a full image reference:
- Explicit tag:
repository/image:latest-developpulls that exact tag. - Explicit digest:
repository/image@sha256:<digest>pulls that exact digest. - Tagless repository: Platform calls ECR
DescribeImages, orders byImagePushedAt, and selects the newest digest for backward compatibility.
A tagless repository URI is not a stable version pin. latest-develop and latest-release are moving branch tags, while build tags and digests are immutable references. The local IDP verification intentionally uses a tagless ava/asol-testingdataprovider repository so every run selects the newest database image by ImagePushedAt. The IDP itself uses latest-develop because the HTTP-only change is available on develop but has not yet been released.
If the exact tagged image already exists locally, Platform may use it without contacting ECR. Credential validity can therefore only be demonstrated when a remote pull is required. Always inspect ServiceRuntimeInfo.ResolvedImage, which contains the actual container image, when a test requires an exact tag.
MongoDB setup
Choose one MongoDB source:
UseRestoredMongoImage()resolves the configuredTestContainers:MongoRepositoryUriand uses a pre-restored database image.WithMongoDumpArchives()restores supported entries fromTestContainers:DumpsPathinto the regularMongoImagecontainer.
UseRestoredMongoImage() takes precedence and clears configured dump paths. The local IDP seed update runs against the resulting MongoDB instance before the IDP container starts.
Configuration precedence
The parameterless TestPlatformBuilder() loads appsettings.json, appsettings.Development.json, and appsettings.test.json, followed by environment variables and then AddUserSecrets(Assembly.GetExecutingAssembly()). The executing assembly is the Core Platform assembly, not automatically the consuming test project. This legacy order is retained for compatibility.
Applications should build and pass their own IConfiguration, as shown above. In the recommended example, JSON is followed by AddUserSecrets<ConsumerTestAssemblyMarker>() and then environment variables, so environment variables have the highest provider priority. The builder uses a supplied configuration as-is.
In-memory values later added by TestContainersWebApplicationFactory configure the in-process test application and are separate from the builder provider order. The factory adds host-side MongoDB, RabbitMQ, Redis, and service base URLs. It does not remove unrelated remote values. Remove stale alpha.avaplace.com and beta.avaplace.com settings or override them explicitly.
Explicit local IDP verification
The real MongoDB, RabbitMQ, and IDP topology is an explicit xUnit v3 test. Run it for net8.0 only:
The test project has its own UserSecretsId, ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests. Configure ECR credentials against that project without adding them to source-controlled JSON:
dotnet user-secrets set 'TestContainers:AwsEcr:AccessKeyId' '<access-key-id>' `
--project 'TestSupport/backend/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests.csproj'
dotnet user-secrets set 'TestContainers:AwsEcr:SecretAccessKey' '<secret-access-key>' `
--project 'TestSupport/backend/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests.csproj'
dotnet user-secrets set 'TestContainers:AwsEcr:SessionToken' '<session-token>' `
--project 'TestSupport/backend/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests.csproj'
Set SessionToken only for temporary session credentials. Environment variables with the same configuration paths remain an alternative and have higher priority in this test's explicitly built configuration.
dotnet test 'TestSupport/backend/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests/ASOL.Core.QA.TestSupport.Testcontainers.Platform.Idp.Tests.csproj' `
--configuration Release `
--framework net8.0 `
-- xUnit.Explicit=only
Normal test runs keep explicit tests disabled. The explicit test requires valid ECR access because resolving the tagless testing-data repository requires a remote metadata query. Missing clients, tenants, images, discovery metadata, signing keys, or token endpoints are test failures, not skips. The test independently verifies that the MongoDB container uses the newest digest returned by ECR.
When using TestPlatformBuilder directly, keep the builder outside the build scope so partially started infrastructure is also removed when BuildAsync fails:
var builder = new TestPlatformBuilder(configuration);
try
{
await using var environment = await builder.BuildAsync(CancellationToken.None);
// Verify the topology.
}
finally
{
await builder.DisposeAsync();
}
DisposeAsync() is idempotent, so the final cleanup is safe after successful environment disposal.
Secret logging
Platform logs environment keys for startup diagnostics, but redacts values whose keys contain secret-shaped fragments such as Secret, Password, Token, AccessKey, ApiKey, ConnectionString, PrivateKey, or EncryptionKey.
Do not rely on redaction as a storage mechanism. Keep sensitive values out of source-controlled configuration.
Troubleshooting
Tests still call alpha or beta
- Inspect user secrets and environment variables.
- Confirm in-memory overrides are added after user secrets.
- Check both service base URLs and authentication authorities.
ECR pull fails
- Verify region, access key, secret key, and session token.
- Confirm the repository URI contains the intended tag or digest.
- Verify that the credentials can access the repository.
A different image starts
- A tagless ECR repository intentionally selects the newest
ImagePushedAtdigest. - Use
latest-releaseorlatest-developfor an intentional moving branch selection, and a build tag or digest for an immutable selection. - Check
ServiceRuntimeInfo.ResolvedImagefor the actualImage.FullNameused by MongoDB, RabbitMQ, Redis, and application services.
Local IDP startup fails
- Verify the restored MongoDB image contains the configured OIDC
ClientIdinIDP.OIDCApplications. - Verify the client exists exactly once and that
MongoSeederImageis available. - Verify the IDP image exposes the configured HTTP port and health endpoint.
- Check the internal MongoDB and RabbitMQ configuration.
- Do not add a local PFX file.
Platform.Idpis HTTP-only by design.
Token acquisition or JWT validation fails
- Use
HostAuthorityfor the host-side token provider. - Use
InternalAuthorityinside service containers. - Configure JWT bearer options through
UseLocalIdp()so discovery, JWKS, and valid issuers use the local runtime information. - Confirm audience/scope and tenant
tidmatch the seeded client permissions. - Register
UseLocalIdpClientTokenProvider()so token acquisition uses its dedicated host-side client instead of an application connector client.
MongoDB restore fails
- Confirm only one restore strategy is selected.
- Verify
DumpsPathresolves from the test process working directory. - Verify the restored image or archive contains the OIDC seed required by the local IDP.
Migration checklist
- Reference
Testcontainers.Platformand only the optional specialized packages the topology needs. - Add non-sensitive
TestContainersdefaults to integration-test appsettings. - Keep machine-specific ECR credentials in user secrets or environment variables.
- Derive the test factory from
TestContainersWebApplicationFactory<TStartup>. - Register regular services through
AddService. - Register the local IDP through
AddLocalIdpinstead of a customIInfrastructureManager. - Use generated
LocalIdpCredentialsfor service container authentication. - Use
LocalIdpRuntimeInfofor host configuration, token acquisition, and valid issuers. - Remove direct usage of the low-level
Testcontainers.Idpworkaround. - Run the targeted integration-test project and verify that no runtime call reaches alpha or beta.
Known boundaries
- Platform does not invent application-specific OIDC clients. The configured client must exist in the selected MongoDB seed.
- Application-specific service lists and environment values remain in the consuming integration-test project.
- Private ECR images and application database dumps require the consumer's normal access credentials.