The SmileHarness interface provides access to various clients that can be used to interact with different aspects of a Smile CDR instance. This document provides detailed information about these clients, their capabilities, and how to use them effectively.
Every client the harness hands out targets its module beneath that module's context_path, so a module configured with context_path=/fhir-request is reached at http://host:port/fhir-request.
See Context Paths.
Where a no-argument accessor has several modules of its type to choose from, it takes the module with the conventional ID (fhir_endpoint for FHIR, smart_auth for SMART) if there is one, and otherwise the first module of that type in the node's configuration.
FHIR clients are used to interact with FHIR resources on the Smile CDR server. The SmileHarness interface provides several methods to obtain FHIR clients with different configurations.
IGenericClient fhirClient = harness.getFhirClient();
This method returns a standard FHIR client that can be used to interact with the FHIR endpoint. By default, this client does not include any authentication, so you may need to add an interceptor for authenticated requests.
getFhirClient(int thePort) - Get a FHIR client for a specific portgetFhirClient(String theModuleId) - Get a FHIR client for a specific modulegetFhirClient(String theModuleId, String theBearerToken) - Get a FHIR client for a specific module that authenticates with an OAuth 2.0 access token, such as one from clientCredentials(...). fhirBearerRequest(moduleId, path, token) is the matching request builder.IGenericClient superuserFhirClient = harness.getSuperuserFhirClient();
This method returns a FHIR client with superuser (admin) credentials already configured. This is useful for administrative tasks or tests that require elevated privileges.
getSuperuserFhirClient(int thePort) - Get a superuser FHIR client for a specific portgetSuperuserFhirClient(String theModuleId) - Get a superuser FHIR client for a specific moduleThe FHIR clients returned by these methods are instances of HAPI FHIR's IGenericClient, which provides a comprehensive API for interacting with FHIR resources. Some common operations include:
client.create().resource(resource).execute()client.read().resource(ResourceType.class).withId(id).execute()client.update().resource(resource).execute()client.delete().resourceById(resourceType, id).execute()client.search().forResource(resourceType).where(criteria).execute()client.transaction().withBundle(bundle).execute()For more information on using HAPI FHIR clients, see the HAPI FHIR documentation.
The Admin JSON client is used to interact with the Smile CDR administrative API, which provides access to configuration, monitoring, and management functions.
AdminJsonRestClient adminClient = harness.getAdminJsonClient();
getAdminJsonClient(int thePort) - Get an Admin JSON client for a specific portThe AdminJsonRestClient provides methods for interacting with various aspects of the Smile CDR administrative API:
adminClient.getNodeConfigurations()adminClient.getModuleInfo(moduleId)adminClient.getPortFromModule(moduleId)adminClient.updateModuleConfig(nodeId, moduleId, options, restart, reload) (waits for restart by default; overload accepts an explicit shouldWaitForRestart flag)adminClient.restartModule(nodeId, moduleId) (waits for restart by default; overload accepts an explicit shouldWaitForRestart flag)The Admin JSON client is particularly useful for:
Both updateModuleConfig(...) and restartModule(...) wait for the module to come back up by default. The short-form overloads block until the target module's first process reports STARTED, so tests can exercise the module immediately after the call returns without racing the restart. Callers that want the old fire-and-forget behavior use the explicit boolean shouldWaitForRestart overloads.
// Default — block until the module is STARTED again.
adminClient.restartModule(nodeId, moduleId);
adminClient.updateModuleConfig(nodeId, moduleId, options, /*restart*/ true, /*reload*/ false);
// Opt out of the wait (fire-and-forget — return as soon as the admin API accepts the request).
adminClient.restartModule(nodeId, moduleId, /*shouldWaitForRestart*/ false);
adminClient.updateModuleConfig(nodeId, moduleId, options, /*restart*/ true, /*reload*/ false, /*shouldWaitForRestart*/ false);
// updateModuleConfig only polls when either 'restart' or 'reload' is also true — otherwise wait is a no-op.
// Override the 90s default with a custom wait timeout (e.g. a longer budget for slow CI).
adminClient.restartModule(nodeId, moduleId, /*shouldWaitForRestart*/ true, Duration.ofMinutes(3));
adminClient.updateModuleConfig(nodeId, moduleId, options, /*restart*/ true, /*reload*/ false, /*shouldWaitForRestart*/ true, Duration.ofMinutes(3));
Polling behavior:
getNodeStatuses() every 500 ms for up to 90 seconds.FAILED_TO_START or FAILED_TO_STOP, an IllegalStateException is thrown immediately rather than waiting for the timeout.STARTED, an IllegalStateException is thrown reporting the last observed status and the number of poll attempts.Pass false when the test does not need to interact with the module immediately after restart, or when you want the caller to be responsible for waiting (e.g. via a custom readiness check). Passing true is the recommended default for most integration tests — it removes the need for ad-hoc Awaitility blocks around module restarts.
The HL7v2 REST client is used to interact with the Smile CDR HL7v2 endpoint, which allows sending and receiving HL7v2 messages.
HL7V2RestClient hl7v2Client = harness.getHL7V2RestClient();
getHL7V2RestClient(int thePort) - Get an HL7v2 REST client for a specific portgetHL7V2RestClient(String theModuleId) - Get an HL7v2 REST client for a specific moduleThe HL7V2RestClient provides methods for sending HL7v2 messages to the Smile CDR server:
hl7v2Client.sendMessage(message)This client is particularly useful for testing HL7v2 integration scenarios, such as:
While not a client per se, the SmileHarness also provides access to the FHIR context, which is useful for working with FHIR resources programmatically.
FhirContext fhirContext = harness.getFhirContext();
getFhirContext(String moduleId) - Get the FHIR context for a specific moduleThe FhirContext provides various utilities for working with FHIR resources:
The Outbound SMART client is used to perform OAuth 2.0 SMART on FHIR authorization flows for testing outbound authentication scenarios.
OutboundSmartClient smartClient = harness.getOutboundSmartClient();
getOutboundSmartClient(int thePort) - Get an Outbound SMART client for a specific portgetOutboundSmartClient(String theModuleId) - Get an Outbound SMART client for a specific moduleThe OutboundSmartClient provides methods for OAuth 2.0 and SMART on FHIR flows:
Server-side flows:
clientCredentials(clientId, clientSecret, scopes...) - Obtain a token, authenticating with a client secret over HTTP Basic authenticationclientCredentialsWithAssertion(clientId, signedJwt, scopes...) - Obtain a token, authenticating with a signed JWT client assertion as a SMART Backend Services client does. The caller builds and signs the JWT.refreshToken(clientId, clientSecret, refreshToken) - Exchange a refresh token, returning the token response as a JSON stringintrospect(token, clientId, clientSecret) - Returns the introspection response; its active member says whether the token is liveopenIdConfiguration() - Returns the .well-known/openid-configuration documentThe two client credentials methods return an AccessTokenResponse record with accessToken(),
tokenType(), expiresIn(), scope(), idToken(), refreshToken() and raw(), the whole response as a JsonNode.
Authorization code flow:
performAuthorizationCodeFlow(...) - Log in as a user, obtain a code and exchange it for an access tokengetAuthorizationCode(...), authorizeBeforeLogin(...), authorizeAfterLogin(...)loginWithPassword(username, password) - Form-based login on the SMART login pageexchangeCode(...) and exchangeCodeWithSecret(...)buildScopeString(), extractCodeFromUrl()OAuth clients themselves are created through the Admin JSON client, with
AdminJsonRestClient.createOAuthClient(nodeId, moduleId, clientDetails).
This client is particularly useful for testing:
The CDS Hooks client calls a CDS Hooks endpoint module (ENDPOINT_CDS_HOOKS), authenticating with the harness credentials.
CdsHooksClient cdsHooksClient = harness.getCdsHooksClient();
getCdsHooksClient(int thePort) - Get a CDS Hooks client for the module configured with a specific portgetCdsHooksClient(String theModuleId) - Get a CDS Hooks client for a specific moduleCdsHooksClient.openAnonymous(baseUrl) - Open a client that sends no credentials, over a connection pool of its own that you must closediscovery() - GET {base}/cds-services, listing the services in its services arrayinvoke(serviceId, request) - POST {base}/cds-services/{id}, returning the response with its cardsfeedback(serviceId, feedback) - POST {base}/cds-services/{id}/feedbackRequests and responses are Jackson JsonNodes, so a test can build any request the CDS Hooks specification allows and assert on any part of the response.
The NPM Package client is used to interact with Smile CDR's package registry endpoint, which provides access to FHIR Implementation Guide and package management functionality.
This tool is meant for uploading Implementation Guides or other FHIR Packages to the repository. You can find out more about creating your own packages here.
NpmPackageClient npmClient = harness.getNpmPackageClient();
getNpmPackageClient(int thePort) - Get an NPM Package client for a specific portgetNpmPackageClient(String theModuleId) - Get an NPM Package client for a specific moduleThe NpmPackageClient provides methods for installing FHIR packages on the Smile CDR server:
npmClient.installPackageBySpec(packageSpec) - Install a package using a PackageInstallationSpec objectnpmClient.installPackageBySpec(jsonString) - Install a package using a JSON string specificationThe PackageSpecBuilder provides a fluent API for creating package installation specifications:
PackageInstallationSpec spec = PackageSpecBuilder
.name("test-profile")
.version("1.0.0")
.packageUrl("classpath:/test_ig.tgz")
.storeOnly()
.build();
PackageInstallOutcomeJson outcome = npmClient.installPackageBySpec(spec);
Alternative JSON String Usage:
String packageSpecJson = """
{
"name": "my-implementation-guide",
"version": "1.0.0",
"packageUrl": "classpath:/my_ig.tgz",
"installMode": "STORE_AND_INSTALL",
"reloadExisting": false,
"fetchDependencies": true,
"installResourceTypes": ["StructureDefinition", "ValueSet"]
}
""";
PackageInstallOutcomeJson outcome = npmClient.installPackageBySpec(packageSpecJson);
Builder Methods:
name(String) - Set the package name (required)version(String) - Set the package version (required)packageUrl(String) - Set the package URL (optional)storeOnly() - Set install mode to STORE_ONLYstoreAndInstall() - Set install mode to STORE_AND_INSTALLreloadExisting(boolean) - Set whether existing resources should be reloadedfetchDependencies(boolean) - Set whether dependencies should be automatically resolved and installedinstallResourceTypes(String...) - Set specific resource types to install from the packagebuild() - Build the PackageInstallationSpecAdvanced Configuration Example:
PackageInstallationSpec spec = PackageSpecBuilder
.name("advanced-profile")
.version("2.0.0")
.packageUrl("classpath:/advanced_ig.tgz")
.storeAndInstall()
.reloadExisting(false)
.fetchDependencies(true)
.installResourceTypes("StructureDefinition", "ValueSet", "CodeSystem")
.build();
This client is particularly useful for testing:
Every client a harness hands out issues on the harness's own HTTP client, so they share one connection pool and one cookie store. A login performed through one is visible to the next; see Sessions, Cookies and Harness Lifecycle.
AdminJsonRestClient, HL7V2RestClient, NpmPackageClient and CdsHooksClient are AutoCloseable, and what close()
does depends on where the client came from:
harness.getAdminJsonClient(), harness.getHL7V2RestClient(), …) the client
borrows the harness's pool. Closing it is a no-op, so a try-with-resources around one will not shut
down the harness the rest of your test is using.AdminJsonRestClient.open(baseUrl, username, password)) the client owns the pool
it created, and close() releases it. Close these, or each open(...) leaves a pool running for the
life of the JVM. The open prefix is the signal: anything you open is yours to close.To build several clients directly and have them share one pool and one session, create a
SmileTestHttpClient yourself and pass it to each — you own it, so close it when you are done, and the clients built over it need no closing of their own:
try (SmileTestHttpClient httpClient = SmileTestHttpClient.create()) {
AdminJsonRestClient adminClient = AdminJsonRestClient.issuingOn(httpClient, baseUrl, "admin", "password");
NpmPackageClient npmClient = NpmPackageClient.issuingOn(httpClient, baseUrl, "admin", "password");
// a session established through adminClient is visible to npmClient
}
open(...) and issuingOn(...) exist on AdminJsonRestClient, HL7V2RestClient, NpmPackageClient
and CdsHooksClient, and all but HL7V2RestClient also have openAnonymous(...); for an unauthenticated HL7v2 client, pass null credentials to open(...). The name tells you at the call site which of the two owns the pool. Working through a SmileHarness needs none of this — the harness already shares its client with everything it hands out.
The older build(...) and buildAnonymous(...) factories still work and are deprecated, because neither name said who owned the pool. Replace build(url, user, pass) with open(...) and
buildAnonymous(url) with openAnonymous(...).
Redirects are not followed by any of these clients, so a 3xx is returned rather than resolved.
The SmileHarness interface includes comments about potential future clients that may be added:
These clients are not yet implemented but may be added in future versions of the library.
When working with these clients, consider the following best practices:
For more recommendations on effective testing with SmileCdrContainer and SmileHarness, see Best Practices.