Usage - Smile CDR Documentation
SmileCdrContainer and SmileHarness Usage
This document provides examples and common usage patterns for working with SmileCdrContainer and SmileHarness in your tests.
Basic Setup with JUnit 5
The most common way to use SmileCdrContainer is with JUnit 5's @Testcontainers and @Container annotations:
import ca.cdr.test.extensions.SmileCdrContainer;
import ca.cdr.test.app.harness.api.SmileHarness;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@Testcontainers
class MyTest {
// Define the Docker tag to use
static String smilecdrVersion = "2025.02.R02";
// Create and configure the container
@Container
private static SmileCdrContainer container = new SmileCdrContainer(smilecdrVersion);
// Store the harness for use in tests
private static SmileHarness harness;
@BeforeAll
public static void setup() {
// Get the harness from the container
harness = container.getHarness();
}
@Test
public void testSomething() {
// Use the harness to interact with Smile CDR
}
}
Copy
SmileCdrContainer Configuration Options
Using Different Smile CDR Versions
You can specify which version of Smile CDR to use, which remote docker registry to use, and which tag to use:
// Using a default version of Smile CDR that the SmileCdrContainer selects.
SmileCdrContainer container = new SmileCdrContainer();
// Using the default registry with a specific Smile CDR version
SmileCdrContainer container = new SmileCdrContainer("2025.02.R02");
// Using a fully custom Docker registry, image, and tag. This can be useful if you have customized
// the base docker image and host it yourself.
DockerImageName customImage = DockerImageName.parse("my-registry.com/my-image:my-tag");
SmileCdrContainer container = new SmileCdrContainer(customImage);
Copy
Using a Custom Properties File
You can provide a custom properties file to configure the Smile CDR instance:
SmileCdrContainer container = new SmileCdrContainer()
.withPropertiesFile("my-custom-config.properties");
Copy
The properties file must be available on the classpath. Please see the tutorial for examples.
Using a Custom Harness Context
By default, the SmileCdrContainer will assume several things to bootstrap connectivity to the Smile CDR container. Specifically it assumes:
- The port for the Admin JSON module is accessible via HTTP on port 9000 (the default).
- Basic authentication is enabled.
- The default username and password are used.
If the configuration you are using does not match the above, you must provide the new information to the Harness Context before building a Smile Harness
You can provide a custom HarnessContext to configure how the harness connects to Smile CDR:
String baseUrl = "http://localhost";
Integer adminJsonPort = 19000;
String adminUsername = "my-admin";
String adminPassword = "my-random-password";
HarnessContext context = new HarnessContext(baseUrl, adminJsonPort, adminUsername, adminPassword);
SmileCdrContainer container = new SmileCdrContainer("2025.02.R02")
.withCustomHarnessContext(context);
Copy
Pre-seeding Data
You can pre-seed users into the Smile CDR instance using the withPreseedFiles() method. This is useful for setting up test users with specific permissions without having to create them programmatically in each test.
The withPreseedFiles(String... theFiles) method takes a path to a pre-seeding file. The shape of these files can be found in the pre-seeding documentation. The files must be on the classpath, and will be loaded into the classes/config_seeding directory of the Smile CDR container.
You also need to configure the properties file to use the preseeded files.
For examples of how to use this functionality, see the tutorial.
Enabling Remote Debugging
You can enable remote debugging for the Smile CDR instance running in the container using the withDebugEnabled() methods. This allows you to attach a debugger from your IDE to troubleshoot issues or step through code execution.
Basic Debug Mode
The simplest way to enable debugging is to call withDebugEnabled() without parameters. This will enable debug mode and suspend container startup until a debugger is attached:
SmileCdrContainer container = new SmileCdrContainer("2025.02.R02")
.withPropertiesFile("my-config.properties")
.withDebugEnabled();
Copy
When using this method:
- The container will expose port 5005 (fixed mapping) for debug connections
- The container will pause during startup until a debugger is attached
- This allows you to set breakpoints before code execution begins
Debug Mode Without Suspending
If you want to enable debugging but allow the container to start without waiting for a debugger, you can pass false to the method:
SmileCdrContainer container = new SmileCdrContainer("2025.02.R02")
.withPropertiesFile("my-config.properties")
.withDebugEnabled(false);
Copy
When using withDebugEnabled(false):
- The container will expose port 5005 for debug connections
- The container will start normally without waiting for a debugger
- You can attach a debugger at any time after the container has started
Connecting Your Debugger To connect IntelliJ IDEA debugger to the container:
- Go to Run → Edit Configurations → Add New → Remote JVM Debug
- Set Host:
localhost(or your container host) - Set Port:
5005(always fixed to this port) - Set Debugger mode: Attach to remote JVM
- Click Debug - the container will resume startup once the debugger connects (if using suspend mode)
Important Notes:
- Port 5005 must be available on the host machine
- When using
withDebugEnabled()(suspend mode), the container will not complete startup until a debugger connects - The debug port is always fixed to 5005 - no need to query for the mapped port
Note that the cdr-interceptor-starterproject comes with a built-in debugger configuration xml file, which intellij can load.
Working with SmileHarness
Once you have a SmileHarness instance, you can use it to interact with the Smile CDR instance in various ways.
Using FHIR Clients
// Get a FHIR client
IGenericClient fhirClient = harness.getFhirClient();
// Add authentication if needed
BasicAuthInterceptor authInterceptor = new BasicAuthInterceptor("username", "password");
// Register the interceptor
fhirClient.registerInterceptor(authInterceptor);
// Use the client to interact with FHIR resources
Bundle bundle = fhirClient.search()
.forResource("Patient")
.returnBundle(Bundle.class)
.execute();
Copy
Using the Admin JSON Client
// Get an Admin JSON client
AdminJsonRestClient adminClient = harness.getAdminJsonClient();
// Use the client to interact with the Admin API
NodeConfigurations config = adminClient.getNodeConfigurations();
Copy
Making Raw HTTP Requests
The typed clients above cover the common cases. When you need to assert on the HTTP exchange itself, the harness gives you a request builder that reads the whole exchange into memory and hands back a response you can assert against freely — a status code, a response header, a redirect, a response body.
// Against the FHIR endpoint, authenticated with the harness credentials
harness.fhirRequest("/Patient/123")
.get()
.assertStatus(200);
// Without credentials, to check that the endpoint rejects anonymous callers
harness.fhirAnonymousRequest("/Patient/123")
.get()
.assertStatus(403);
// Against a named FHIR endpoint module
harness.fhirRequest("my_endpoint_module", "/Patient/123").get();
Copy
Requests are not limited to FHIR. Any port the harness can reach works, and the harness resolves the container port mapping for you, so you pass the port the module is configured with:
// Admin JSON
harness.adminJsonRequest("/module-status/").get().assertStatus(200);
// Any module, by configured port or by module ID
harness.request(8002, "/packages").get();
harness.request("endpoint_package_registry", "/packages").get();
// Without credentials
harness.anonymousRequest(8000, "/metadata").get().assertStatus(403);
Copy
Every builder returns an HttpTestRequest, so you can add headers and choose a verb and body:
Patient patient = new Patient();
patient.setActive(true);
HttpTestResponse response = harness.fhirRequest("/Patient")
.withHeader("Prefer", "return=representation")
.post(patient);
response.assertStatus(201);
assertThat(response.getHeader("Location")).contains("Patient/");
assertThat(response.getBody()).contains("\"active\": true");
Copy
Requesting as a different user
To assert how some other user is treated, build the request with an anonymous builder and add that user's credentials, rather than overriding the credentials on fhirRequest(...):
harness.fhirAnonymousRequest("/Patient/123")
.withBasicAuth("limited-user", "password")
.get()
.assertStatus(403);
Copy
withBasicAuth(...) replaces the Authorization header, but it does not drop the shared session cookie described in Sessions, Cookies and Harness Lifecycle.
Adding it to fhirRequest(...) therefore sends the new credentials alongside whatever session an earlier request established, which is not the request you meant to describe. The anonymous builders carry no cookies, so they send exactly the credentials you gave them and nothing else.
Each has a named-module and a port form: fhirAnonymousRequest(moduleId, path),
anonymousRequest(port, path) and anonymousRequest(moduleId, path).
Redirects are not followed, so a 302 comes back as a 302 and you can assert on its
Location header rather than on wherever it pointed.
Paths must begin with a slash. The single argument to fhirRequest(...) and
fhirAnonymousRequest(...) is a path, while the single argument to getFhirClient(...),
getHL7V2RestClient(...) and getFhirContext(...) is a module ID — so passing a module ID to a request builder is rejected outright rather than issued as a path against the default module. The one exception is the empty path, which addresses the module's base URL:
harness.fhirRequest("").post(transactionBundle).assertStatus(200);
Copy
HttpTestRequest and HttpTestResponse come from HAPI FHIR's hapi-fhir-test-utilities
(ca.uhn.fhir.test.utilities), which cdr-public-test-utils brings in for you.
Using the HL7v2 REST Client
The HL7V2RestClient allows you to send HL7v2 messages to Smile CDR and process the responses. There are several ways to obtain and use this client.
Getting an HL7v2 Client
// Get an HL7v2 REST client using the default port (7000)
HL7V2RestClient hl7v2Client = harness.getHL7V2RestClient();
// Get an HL7v2 REST client for a specific port
HL7V2RestClient hl7v2ClientWithPort = harness.getHL7V2RestClient(7001);
// Get an HL7v2 REST client for a specific module ID
HL7V2RestClient hl7v2ClientWithModuleId = harness.getHL7V2RestClient("hl7_endpoint_two");
Copy
A client obtained from the harness issues on the harness's HTTP client, so it shares the session described in Sessions, Cookies and Harness Lifecycle. You can also open one directly with HL7V2RestClient.open(baseUrl, username, password), which stands up a connection pool of its own that you then have to close. Either way redirects are not followed, so a 3xx comes back as a
3xx rather than being resolved for you.
Sending a String Message You can send an HL7v2 message as a raw string:
// Create a raw HL7v2 message as a string
String rawMessage = "MSH|^~\\&|TestApp|TestFacility|SmileCDR|SmileCDR|20230101120000||ADT^A01^ADT_A01|123456|P|2.5\r" +
"PID|||12345||Smith^John||19800101|M";
// Send the message to the server
Message response = hl7v2Client.sendMessage(rawMessage);
// Verify the response is an acknowledgment
assertThat(response.getName()).contains("ACK");
Copy
Creating and Sending an ADT Message ADT (Admission, Discharge, Transfer) messages are commonly used in healthcare systems. Here's how to create and send an ADT_A01 message:
// Create a sample ADT_A01 (admission) message
ADT_A01 adt = new ADT_A01();
MSH msh = adt.getMSH();
// Set required MSH fields
msh.getFieldSeparator().setValue("|");
msh.getEncodingCharacters().setValue("^~\\&");
msh.getSendingApplication().getNamespaceID().setValue("TestApp");
msh.getSendingFacility().getNamespaceID().setValue("TestFacility");
msh.getReceivingApplication().getNamespaceID().setValue("SmileCDR");
msh.getReceivingFacility().getNamespaceID().setValue("SmileCDR");
msh.getDateTimeOfMessage().getTime().setValue("20230101120000");
msh.getMessageType().getMessageCode().setValue("ADT");
msh.getMessageType().getTriggerEvent().setValue("A01");
msh.getMessageType().getMessageStructure().setValue("ADT_A01");
msh.getMessageControlID().setValue("123456");
msh.getProcessingID().getProcessingID().setValue("P");
msh.getVersionID().getVersionID().setValue("2.5");
// Set patient information
adt.getPID().getPatientID().getIDNumber().setValue("12345");
adt.getPID().getPatientName(0).getFamilyName().getSurname().setValue("Smith");
adt.getPID().getPatientName(0).getGivenName().setValue("John");
adt.getPID().getDateTimeOfBirth().getTime().setValue("19800101");
adt.getPID().getAdministrativeSex().setValue("M");
// Send the message to the server
Message response = hl7v2Client.sendMessage(adt);
// Process the response
if (response.getName().contains("ACK")) {
// Handle acknowledgment
System.out.println("Message acknowledged");
} else {
// Handle other response types
System.out.println("Received response: " + response.getName());
}
Copy
Using the Test Data Helper Smile CDR provides a test data helper class that can generate sample HL7v2 messages for you:
// Get the FHIR context from the harness
FhirContext fhirContext = harness.getFhirContext();
// Create a test data helper
Hl7V2TestDataHelper testDataHelper = Hl7V2TestDataHelper.buildDefault(fhirContext);
// Create a sample ADT_A01 message using the helper
ADT_A01 adtA01 = testDataHelper.createAdtA01();
// Send the message to the server
Message response = hl7v2Client.sendMessage(adtA01);
Copy
The Hl7V2TestDataHelper provides methods for creating various types of HL7v2 messages with realistic test data, saving you the effort of manually populating all the required fields.
Using the Outbound SMART Client
The OutboundSmartClient allows you to test OAuth 2.0 SMART on FHIR authorization flows. Currently, the client provides method stubs for testing purposes.
Getting an Outbound SMART Client
// Get an Outbound SMART client using auto-discovery
OutboundSmartClient smartClient = harness.getOutboundSmartClient();
// Get an Outbound SMART client for a specific port
OutboundSmartClient smartClientWithPort = harness.getOutboundSmartClient(8000);
// Get an Outbound SMART client for a specific module ID
OutboundSmartClient smartClientWithModuleId = harness.getOutboundSmartClient("SECURITY_OUT_SMART");
Copy
Testing OAuth Flows****Using Implemented Methods
// Test parameters
String clientId = "test-client-id";
String clientSecret = "test-client-secret";
String redirectUrl = "https://client.example.org/cb";
String[] scopes = {"patient/*.read", "user/*.read"};
String state = "test-state-12345";
// Perform complete OAuth authorization code flow
String accessToken = smartClient.oauthAuthorizationCodeFlow(
clientId, redirectUrl, scopes, state, clientSecret);
// Alternative: Get authorization code first, then exchange for token
String authCode = smartClient.getAuthorizationCode(clientId, redirectUrl, scopes, state);
String token = smartClient.exchangeCodeWithSecretHttpBasic(clientId, clientSecret, authCode);
// Create OAuth clients for testing
smartClient.createOidcClient("unit_test_node", "smart_auth", clientId, clientSecret,
scopes, false, false, false);
// Utility methods
String scopeString = smartClient.buildScopeString(scopes);
String extractedCode = OutboundSmartClient.extractCodeFromUrl("https://example.com/cb?code=abc123&state=xyz");
Copy
Testing API Stubs (Not Yet Implemented)
// These methods currently throw UnsupportedOperationException
assertThatThrownBy(() -> {
smartClient.requestAuthorizationCode(clientId, clientSecret, redirectUrl, scopes, state);
}).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> {
smartClient.requestAuthorizationClientToken(clientId, clientSecret);
}).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> {
smartClient.refreshToken(clientId, clientSecret, "refresh_token_value");
}).isInstanceOf(UnsupportedOperationException.class);
Copy
Sessions, Cookies and Harness Lifecycle
A harness owns one HTTP client: one connection pool and one cookie store. Every client it hands out issues on that client — the request builders, the Admin JSON client, the outbound SMART client, the package registry client and the HL7v2 client alike. The shared cookie store is what makes a multi-request login flow work, so a SMART login driven through getOutboundSmartClient() is visible to a later fhirRequest(...).
Cookies are scoped to the host and not to the port, so a session established against one module's port travels to the others. To start a fresh session, clear them:
harness.clearCookies();
Copy
fhirAnonymousRequest(...) and anonymousRequest(...) are the exception: they send no cookies and store none. That is what makes them usable for the assertion they exist for — that a caller with no credentials is turned away — since a request on the shared session would be answered as whoever logged in last:
harness.getOutboundSmartClient().loginWithPassword("some-user", "some-password"); // establishes a session
harness.fhirAnonymousRequest("/Patient/123").get().assertStatus(403); // still anonymous
Copy
The FHIR clients returned by getFhirClient(...) are HAPI IGenericClients with their own transport, and are not covered by any of this.
SmileHarness is AutoCloseable. When you obtain it from a SmileCdrContainer you do not need to close it yourself — the container closes it when it stops, and if you close it anyway the next
getHarness() builds a replacement rather than handing back the closed one. Close it yourself only when you built the harness directly:
try (SmileHarness harness = new LocalhostSmileHarness(context)) {
// ...
}
Copy
Refreshing after a configuration change
container.getHarness() caches the harness, and a harness reads the node's module configuration once, when it is built — module IDs, ports and FHIR versions all come from that snapshot, which is why a request builder costs no Admin JSON round trip. If a test adds, removes or re-ports a module through the Admin JSON API, tell the container to rebuild the harness — otherwise the next call returns the same pre-change snapshot:
harness.getAdminJsonClient().moduleCreate(/* ... */);
harness = container.refreshHarness();
Copy
refreshHarness() returns the replacement and caches it, so a later getHarness() gives you the same one. It closes the previous harness, ending any session it held and invalidating any reference still pointing at it — use the harness it hands back rather than the one you were holding.
withCustomHarnessContext(...) discards the cached harness in the same way, so a context set after the first getHarness() still takes effect.
getFhirContext(...) is the exception: it queries the node each time it is called, so it reflects a configuration change without a refresh.
Working with FHIR Contexts
// Get the FHIR context from the first found persistence module
FhirContext fhirContext = harness.getFhirContext();
// Get the FHIR context from a named persistence module
String persistenceModuleId = "my_persistence_r4_module";
FhirContext fhirContext = harness.getFhirContext(persistenceModuleId);
Copy
Port Mapping
When working with Docker containers, port mapping is important to understand. The SmileCdrContainer maps container ports to host ports, and the SmileHarness handles this mapping for you:
// Get a FHIR client for a specific port
IGenericClient fhirClient = harness.getFhirClient(8000);
// The harness automatically maps the container port to the host port
String serverBase = fhirClient.getServerBase();
// serverBase will be something like "http://localhost:32789"
Copy
You can also get the port mapping directly from the container:
Integer mappedFhirPort = container.getMappedPort(8000);
Copy
Working with Multiple Modules
If your Smile CDR instance has multiple modules of the same type (e.g., multiple FHIR endpoints), you can specify which one to use:
// Get a FHIR client for a specific module by ID
IGenericClient fhirClient = harness.getFhirClient("my_endpoint_module");
// Get a FHIR client for a specific module by port
IGenericClient fhirClient = harness.getFhirClient(8010);
// Get a FHIR context for a specific module by ID
FhirContext fhirContext = harness.getFhirContext("my_persistence_module");
Copy
Next Steps
For a step-by-step guide to writing tests with SmileCdrContainer and SmileHarness, see the tutorial.
For detailed information about the clients available through SmileHarness, see Available Clients.
For recommendations on effective testing with SmileCdrContainer and SmileHarness, see Best Practices.
Usage
Testing Tools