Přeskočit na hlavní obsah

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, and TestContainersWebApplicationFactory<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-side LocalIdpClientTokenProvider and UseLocalIdpClientTokenProvider() 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 for Platform.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.
  • ClientId and ClientSecret - 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-develop pulls that exact tag.
  • Explicit digest: repository/image@sha256:<digest> pulls that exact digest.
  • Tagless repository: Platform calls ECR DescribeImages, orders by ImagePushedAt, 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 configured TestContainers:MongoRepositoryUri and uses a pre-restored database image.
  • WithMongoDumpArchives() restores supported entries from TestContainers:DumpsPath into the regular MongoImage container.

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 ImagePushedAt digest.
  • Use latest-release or latest-develop for an intentional moving branch selection, and a build tag or digest for an immutable selection.
  • Check ServiceRuntimeInfo.ResolvedImage for the actual Image.FullName used by MongoDB, RabbitMQ, Redis, and application services.

Local IDP startup fails

  • Verify the restored MongoDB image contains the configured OIDC ClientId in IDP.OIDCApplications.
  • Verify the client exists exactly once and that MongoSeederImage is 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.Idp is HTTP-only by design.

Token acquisition or JWT validation fails

  • Use HostAuthority for the host-side token provider.
  • Use InternalAuthority inside service containers.
  • Configure JWT bearer options through UseLocalIdp() so discovery, JWKS, and valid issuers use the local runtime information.
  • Confirm audience/scope and tenant tid match 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 DumpsPath resolves from the test process working directory.
  • Verify the restored image or archive contains the OIDC seed required by the local IDP.

Migration checklist

  1. Reference Testcontainers.Platform and only the optional specialized packages the topology needs.
  2. Add non-sensitive TestContainers defaults to integration-test appsettings.
  3. Keep machine-specific ECR credentials in user secrets or environment variables.
  4. Derive the test factory from TestContainersWebApplicationFactory<TStartup>.
  5. Register regular services through AddService.
  6. Register the local IDP through AddLocalIdp instead of a custom IInfrastructureManager.
  7. Use generated LocalIdpCredentials for service container authentication.
  8. Use LocalIdpRuntimeInfo for host configuration, token acquisition, and valid issuers.
  9. Remove direct usage of the low-level Testcontainers.Idp workaround.
  10. 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.