# 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:

```java
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:

```java

// 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:

```java
SmileCdrContainer container = new SmileCdrContainer()
    .withPropertiesFile("my-custom-config.properties");
```

Copy

The properties file must be available on the classpath. Please see the [tutorial](/content/docs/guide_tutorials/tutorial_integration_test.html) 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:

```java
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](/content/docs/guide_installation/pre_seeding.html). 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](/content/docs/guide_tutorials/tutorial_integration_test.html).

## 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:

```java
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:

```java
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:

1. Go to **Run → Edit Configurations → Add New → Remote JVM Debug**
2. Set **Host**: `localhost` (or your container host)
3. Set **Port**: `5005` (always fixed to this port)
4. Set **Debugger mode**: Attach to remote JVM
5. 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

```java
// 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

```java
// 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.

```java
// 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:

```java
// 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:

```java
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(...)`:

```java
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](/content/docs/test_tools/usage.html#sessions-cookies-and-harness-lifecycle/index.html).
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:

```java
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**

```java
// 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](/content/docs/test_tools/usage.html#sessions-cookies-and-harness-lifecycle/index.html). 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:

```java
// 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:

```java
// 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:

```java
// 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**

```java
// 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**

```java
// 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)**

```java
// 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:

```java
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:

```java
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 `IGenericClient`s 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:

```java
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:

```java
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

```java
// 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:

```java
// 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:

```java
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:

```java
// 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](/content/docs/guide_tutorials/tutorial_integration_test.html).

For detailed information about the clients available through `SmileHarness`, see [Available Clients](/content/docs/test_tools/clients.html).

For recommendations on effective testing with `SmileCdrContainer` and `SmileHarness`, see [Best Practices](/content/docs/test_tools/bestpractices.html).

[Overview](/content/docs/test_tools/overview.html)

Usage

Testing Tools

[Overview](/content/docs/test_tools/overview.html)
[Usage](/content/docs/test_tools/usage.html)
[Clients](/content/docs/test_tools/clients.html)
[Best Practices](/content/docs/test_tools/bestpractices.html)

[Clients](/content/docs/test_tools/clients.html)
