001/*- 002 * #%L 003 * Smile CDR - CDR 004 * %% 005 * Copyright (C) 2016 - 2026 Smile CDR, Inc. 006 * %% 007 * All rights reserved. 008 * #L% 009 */ 010package ca.cdr.test.app.harness.api; 011 012import ca.cdr.test.app.clients.AdminJsonRestClient; 013import ca.cdr.test.app.clients.CdrFhirClient; 014import ca.cdr.test.app.clients.CdsHooksClient; 015import ca.cdr.test.app.clients.HL7V2RestClient; 016import ca.cdr.test.app.clients.NpmPackageClient; 017import ca.cdr.test.app.clients.OutboundSmartClient; 018import ca.cdr.test.app.clients.SmartHealthLinkClient; 019import ca.cdr.test.app.clients.SmilePortalClient; 020import ca.cdr.test.app.clients.common.ISmileTestHttpClient; 021import ca.uhn.fhir.context.FhirContext; 022import ca.uhn.fhir.rest.client.api.IGenericClient; 023import ca.uhn.fhir.test.utilities.HttpTestRequest; 024import jakarta.annotation.Nonnull; 025 026/** 027 * Interface for interacting with a Smile CDR instance during testing. It hands out the clients a 028 * test needs, in four kinds: 029 * <ul> 030 * <li><b>Request builders</b> ? {@link #fhirRequest(String)}, {@link #request(int, String)} and 031 * {@link #adminJsonRequest(String)}. Reach for these first. They resolve the module's port, add 032 * the harness credentials, and hand back the whole exchange, so a test can assert on a status 033 * code, a response header or a redirect. {@link #fhirAnonymousRequest(String)} and 034 * {@link #anonymousRequest(int, String)} are their unauthenticated counterparts.</li> 035 * <li><b>FHIR clients</b> ? {@link #getFhirClient()} and {@link #getSuperuserFhirClient()}. Use 036 * these to <em>do</em> FHIR work (read, search, transact) rather than to assert on HTTP.</li> 037 * <li><b>Protocol clients</b> ? {@link #getAdminJsonClient()}, {@link #getHL7V2RestClient()}, 038 * {@link #getNpmPackageClient()}, {@link #getOutboundSmartClient()} and 039 * {@link #getCdsHooksClient()}. Each wraps one API, so a test does not have to hand-build its 040 * requests.</li> 041 * <li><b>The raw HTTP client</b> ? {@link #getHttpClient()}, for a URL this harness cannot build, 042 * such as a service outside the CDR node.</li> 043 * </ul> 044 * A harness owns a single HTTP client, so the request builders and the protocol clients all share 045 * one cookie store and one connection pool. That shared store is what lets a multi-request login 046 * flow work across them: a SMART login is visible to a later {@link #fhirRequest(String)}. Cookies 047 * are scoped to the host rather than the port, so the session reaches every module. 048 * {@link #clearCookies()} ends it; {@link #close()} releases the pool. The FHIR clients are the 049 * exception ? they are HAPI {@link IGenericClient}s with their own transport, and none of this 050 * applies to them. 051 * <p> 052 * Every URL a harness builds includes the target module's {@code context_path}, so a module 053 * mounted at {@code /fhir-request} is reached there by the request builders and the clients alike; 054 * paths passed to the request builders are relative to it. 055 * <p> 056 * Where a no-argument accessor has several modules of its type to choose from, it takes the one 057 * with the conventional ID ({@code fhir_endpoint} for FHIR, {@code smart_auth} for SMART) if there 058 * is one, and otherwise the first in the node's configuration. 059 * <p> 060 * {@link #fhirAnonymousRequest(String)} and {@link #anonymousRequest(int, String)} opt out of the 061 * session too: they send no cookies and store none, so a test asserting that an unauthenticated 062 * caller is rejected cannot be answered as whoever logged in last. 063 */ 064public interface SmileHarness extends AutoCloseable { 065 /** 066 * Gets an administrative JSON client for interacting with the CDR's admin API. 067 * This will create an {@link AdminJsonRestClient} with the first available ADMIN_JSON module 068 * u 069 * 070 * @return An autodiscovered AdminJsonRestClient. 071 */ 072 AdminJsonRestClient getAdminJsonClient(); 073 074 /** 075 * Gets an {@link AdminJsonRestClient} for interacting with the CDR's admin API on a specific port. 076 * 077 * @param thePort The port to connect to 078 * @return The admin JSON client configured with the specified port 079 */ 080 AdminJsonRestClient getAdminJsonClient(int thePort); 081 082 /** 083 * Gets a FHIR client with superuser privileges. 084 * This will create an {@link IGenericClient} with the first available FHIR_ENDPOINT module 085 * 086 * @return The FHIR client with superuser authentication 087 */ 088 CdrFhirClient getSuperuserFhirClient(); 089 090 /** 091 * Gets a FHIR client with superuser privileges on a specific port. 092 * 093 * @param thePort The port to connect to 094 * @return The FHIR client with superuser authentication on the specified port 095 */ 096 CdrFhirClient getSuperuserFhirClient(int thePort); 097 098 /** 099 * Gets a FHIR client with superuser privileges for a specific module. 100 * 101 * @param theModuleId The ID of the module to connect to 102 * @return The FHIR client with superuser authentication for the specified module 103 */ 104 CdrFhirClient getSuperuserFhirClient(String theModuleId); 105 106 /** 107 * Gets a standard FHIR client. 108 * 109 * @return The FHIR client with default authentication 110 */ 111 CdrFhirClient getFhirClient(); 112 113 /** 114 * Gets a standard FHIR client on a specific port. 115 * 116 * @param thePort The port to connect to 117 * @return The FHIR client with default authentication on the specified port 118 */ 119 CdrFhirClient getFhirClient(int thePort); 120 121 /** 122 * Gets a standard FHIR client for a specific module. 123 * 124 * @param theModuleId The ID of the module to connect to 125 * @return The FHIR client with default authentication for the specified module 126 */ 127 CdrFhirClient getFhirClient(String theModuleId); 128 129 /** 130 * Gets a FHIR client for a specific module that authenticates with an OAuth 2.0 access token, 131 * such as one from {@link OutboundSmartClient#clientCredentials(String, String, String...)}, 132 * rather than with this harness's credentials. 133 * 134 * @param theModuleId The ID of the module to connect to 135 * @param theBearerToken the access token to send as {@code Authorization: Bearer} 136 * @return The FHIR client for the specified module, authenticating with the token 137 */ 138 CdrFhirClient getFhirClient(@Nonnull String theModuleId, @Nonnull String theBearerToken); 139 140 /** 141 * Gets the FHIR context used by this harness. 142 * 143 * @return The FHIR context 144 */ 145 FhirContext getFhirContext(); 146 147 /** 148 * Gets the FHIR context for a specific module. 149 * 150 * @param moduleId The ID of the module 151 * @return The FHIR context for the specified module 152 */ 153 FhirContext getFhirContext(String moduleId); 154 155 /** 156 * Gets an HL7V2 REST client for interacting with the CDR's HL7V2 endpoint. 157 * 158 * @return The HL7V2 REST client configured with default port 159 */ 160 HL7V2RestClient getHL7V2RestClient(); 161 162 /** 163 * Gets an HL7V2 REST client for interacting with the CDR's HL7V2 endpoint on a specific port. 164 * 165 * @param thePort The port to connect to 166 * @return The HL7V2 REST client configured with the specified port 167 */ 168 HL7V2RestClient getHL7V2RestClient(int thePort); 169 170 /** 171 * Gets an HL7V2 REST client for a specific module. 172 * 173 * @param theModuleId The ID of the module to connect to 174 * @return The HL7V2 REST client for the specified module 175 */ 176 HL7V2RestClient getHL7V2RestClient(String theModuleId); 177 178 /** 179 * Gets an outbound SMART client for OAuth 2.0 authorization flows. 180 * This will create an OutboundSmartClient targeting the first available SMART endpoint module. 181 * 182 * @return The outbound SMART client configured with default endpoint 183 */ 184 OutboundSmartClient getOutboundSmartClient(); 185 186 /** 187 * Gets an outbound SMART client for OAuth 2.0 authorization flows on a specific port. 188 * 189 * @param thePort The port to connect to 190 * @return The outbound SMART client configured with the specified port 191 */ 192 OutboundSmartClient getOutboundSmartClient(int thePort); 193 194 /** 195 * Gets an outbound SMART client for OAuth 2.0 authorization flows for a specific module. 196 * 197 * @param theModuleId The ID of the module to connect to 198 * @return The outbound SMART client for the specified module 199 */ 200 OutboundSmartClient getOutboundSmartClient(String theModuleId); 201 202 /** 203 * Gets an NPM Package client for interacting with the CDR's package registry endpoint. 204 * This will create an NpmPackageClient targeting the first available PACKAGE_REGISTRY endpoint module. 205 * 206 * @return The NPM Package client configured with default endpoint 207 */ 208 NpmPackageClient getNpmPackageClient(); 209 210 /** 211 * Gets an NPM Package client for interacting with the CDR's package registry endpoint on a specific port. 212 * 213 * @param thePort The port to connect to 214 * @return The NPM Package client configured with the specified port 215 */ 216 NpmPackageClient getNpmPackageClient(int thePort); 217 218 /** 219 * Gets an NPM Package client for interacting with the CDR's package registry endpoint for a specific module. 220 * 221 * @param theModuleId The ID of the module to connect to 222 * @return The NPM Package client for the specified module 223 */ 224 NpmPackageClient getNpmPackageClient(String theModuleId); 225 226 @Nonnull 227 SmartHealthLinkClient getSmartHealthLinkClient(@Nonnull String theModuleId); 228 229 @Nonnull 230 SmilePortalClient getSmilePortalClient(@Nonnull String theModuleId); 231 232 /** 233 * Gets a CDS Hooks client for the default CDS Hooks endpoint module, authenticating with this 234 * harness's {@link HarnessContext} credentials. For an unauthenticated client, use 235 * {@link CdsHooksClient#openAnonymous(String)}. 236 * 237 * @return The CDS Hooks client for the first discovered {@code ENDPOINT_CDS_HOOKS} module 238 * @throws IllegalStateException if the node has no CDS Hooks endpoint 239 */ 240 @Nonnull 241 CdsHooksClient getCdsHooksClient(); 242 243 /** 244 * Gets a CDS Hooks client for the module configured with a specific port. 245 * 246 * @param thePort The port the module is configured with 247 * @return The CDS Hooks client for that module 248 */ 249 @Nonnull 250 CdsHooksClient getCdsHooksClient(int thePort); 251 252 /** 253 * Gets a CDS Hooks client for a specific module. 254 * 255 * @param theModuleId The ID of the module to connect to 256 * @return The CDS Hooks client for the specified module 257 */ 258 @Nonnull 259 CdsHooksClient getCdsHooksClient(@Nonnull String theModuleId); 260 261 /** 262 * Starts building a request against the first discovered FHIR endpoint module, pre-authenticated 263 * with this harness's {@link HarnessContext} credentials via HTTP Basic Auth. 264 * <p> 265 * The single argument here is a <b>path</b>, unlike the single argument to 266 * {@link #getFhirClient(String)} and its siblings, which is a module ID. To target a named module 267 * use {@link #fhirRequest(String, String)}. 268 * 269 * @param thePath the path below the FHIR endpoint's base URL, beginning with a slash 270 * @return A {@link HttpTestRequest} builder targeting the discovered FHIR endpoint 271 * @throws IllegalArgumentException if the path is neither empty nor beginning with a slash ? the 272 * shape a module ID passed here by mistake would have 273 */ 274 @Nonnull 275 HttpTestRequest fhirRequest(@Nonnull String thePath); 276 277 /** 278 * Starts building a request against a specific FHIR endpoint module, pre-authenticated 279 * with this harness's {@link HarnessContext} credentials via HTTP Basic Auth. 280 * 281 * @param theModuleId The ID of the FHIR endpoint module to target 282 * @param thePath the path below the FHIR endpoint's base URL, beginning with a slash 283 * @return A {@link HttpTestRequest} builder targeting the specified FHIR endpoint module 284 */ 285 @Nonnull 286 HttpTestRequest fhirRequest(@Nonnull String theModuleId, @Nonnull String thePath); 287 288 /** 289 * Starts building an unauthenticated request against the first discovered FHIR endpoint module. 290 * It carries neither credentials nor cookies, so it cannot be answered as whoever logged in last. 291 * Add credentials with {@link HttpTestRequest#withBasicAuth(String, String)} if you need them. 292 * <p> 293 * The single argument here is a <b>path</b>; to target a named module use 294 * {@link #fhirAnonymousRequest(String, String)}. 295 * 296 * @param thePath the path below the FHIR endpoint's base URL, beginning with a slash 297 * @return A {@link HttpTestRequest} builder targeting the discovered FHIR endpoint, with no authentication 298 * @throws IllegalArgumentException if the path is neither empty nor beginning with a slash ? the 299 * shape a module ID passed here by mistake would have 300 */ 301 @Nonnull 302 HttpTestRequest fhirAnonymousRequest(@Nonnull String thePath); 303 304 /** 305 * Starts building an unauthenticated request against a specific FHIR endpoint module. It carries 306 * neither credentials nor cookies, so it cannot be answered as whoever logged in last. 307 * <p> 308 * This is the builder to use when asserting how a <em>different</em> user is treated: 309 * {@code fhirAnonymousRequest(moduleId, path).withBasicAuth(user, password)} sends that user's 310 * credentials and nothing else. Adding credentials to {@link #fhirRequest(String, String)} 311 * instead would leave the shared session cookie on the request alongside them. 312 * 313 * @param theModuleId The ID of the FHIR endpoint module to target 314 * @param thePath the path below the FHIR endpoint's base URL, beginning with a slash 315 * @return A {@link HttpTestRequest} builder targeting the specified module, with no authentication 316 */ 317 @Nonnull 318 HttpTestRequest fhirAnonymousRequest(@Nonnull String theModuleId, @Nonnull String thePath); 319 320 @Nonnull 321 HttpTestRequest serverUrlRequest(@Nonnull String theServerUrl); 322 323 /** 324 * Starts building a request against a specific FHIR endpoint module that authenticates with an 325 * OAuth 2.0 access token. Like {@link #fhirAnonymousRequest(String, String)}, it carries neither 326 * this harness's credentials nor its cookies; it adds {@code Authorization: Bearer} instead. 327 * 328 * @param theModuleId The ID of the FHIR endpoint module to target 329 * @param thePath the path below the FHIR endpoint's base URL, beginning with a slash 330 * @param theBearerToken the access token to send 331 * @return A {@link HttpTestRequest} builder targeting the specified module 332 */ 333 @Nonnull 334 HttpTestRequest fhirBearerRequest( 335 @Nonnull String theModuleId, @Nonnull String thePath, @Nonnull String theBearerToken); 336 337 /** 338 * Starts building a request against any port this harness can reach, pre-authenticated with this 339 * harness's {@link HarnessContext} credentials via HTTP Basic Auth. Pass the port the module is 340 * configured with; the harness maps it to the port the module is published on. 341 * <p> 342 * This carries no {@link FhirContext} and so cannot encode a FHIR resource body ? use 343 * {@link #fhirRequest(String)} for that. 344 * 345 * @param thePort the port the target module is configured with; a port no module is configured 346 * with is addressed at its root 347 * @param thePath the path below that module's base URL, beginning with a slash 348 * @return A {@link HttpTestRequest} builder targeting the given port 349 */ 350 @Nonnull 351 HttpTestRequest request(int thePort, @Nonnull String thePath); 352 353 /** 354 * Starts building a request against a specific module, resolving that module's port from the 355 * running node's configuration. 356 * 357 * @param theModuleId The ID of the module to target 358 * @param thePath the path below that module's base URL, beginning with a slash 359 * @return A {@link HttpTestRequest} builder targeting the specified module 360 * @see #request(int, String) 361 */ 362 @Nonnull 363 HttpTestRequest request(@Nonnull String theModuleId, @Nonnull String thePath); 364 365 /** 366 * Starts building a request against the Admin JSON API, pre-authenticated with this harness's 367 * {@link HarnessContext} credentials via HTTP Basic Auth. 368 * <p> 369 * A {@link HarnessContext} naming no Admin JSON port falls back to the default one, which is the 370 * port this harness already read the node's configuration from. The path is relative to 371 * {@link HarnessContext#jsonAdminContextPath()}. 372 * 373 * @param thePath the path below the Admin JSON base URL, beginning with a slash 374 * @return A {@link HttpTestRequest} builder targeting the Admin JSON API 375 */ 376 @Nonnull 377 HttpTestRequest adminJsonRequest(@Nonnull String thePath); 378 379 /** 380 * Starts building an unauthenticated request against any port this harness can reach. It carries 381 * neither credentials nor cookies, so it cannot be answered as whoever logged in last. Add 382 * credentials with {@link HttpTestRequest#withBasicAuth(String, String)} if you need them. 383 * 384 * @param thePort the port the target module is configured with 385 * @param thePath the path below that module's base URL, beginning with a slash 386 * @return A {@link HttpTestRequest} builder targeting the given port, with no authentication 387 */ 388 @Nonnull 389 HttpTestRequest anonymousRequest(int thePort, @Nonnull String thePath); 390 391 /** 392 * Starts building an unauthenticated request against a specific module, resolving that module's 393 * port the way {@link #request(String, String)} does. It carries neither credentials nor 394 * cookies, so it cannot be answered as whoever logged in last. Add credentials with 395 * {@link HttpTestRequest#withBasicAuth(String, String)} to assert how a particular user is 396 * treated without the shared session riding along. 397 * 398 * @param theModuleId The ID of the module to target 399 * @param thePath the path below that module's base URL, beginning with a slash 400 * @return A {@link HttpTestRequest} builder targeting the specified module, with no authentication 401 */ 402 @Nonnull 403 HttpTestRequest anonymousRequest(@Nonnull String theModuleId, @Nonnull String thePath); 404 405 /** 406 * Returns this harness's HTTP client as a handle that can issue requests but cannot close it. 407 * Use it only for a URL this harness cannot build ? a service outside the CDR node, or a port it 408 * did not discover. Prefer {@link #fhirRequest(String)} and {@link #request(String, String)}, 409 * which resolve the module's port and build the URL for you. 410 * <p> 411 * {@link ISmileTestHttpClient#request(String)} shares this harness's session and pool; 412 * {@link ISmileTestHttpClient#cookielessRequest(String)} shares only the pool. 413 * 414 * @return a handle whose lifetime is this harness's ? {@link #close()} ends it 415 */ 416 @Nonnull 417 ISmileTestHttpClient getHttpClient(); 418 419 /** 420 * Discards every cookie held by this harness's HTTP client, ending any session established by 421 * prior requests. Tests exercising several login flows in sequence need this between flows. 422 */ 423 void clearCookies(); 424 425 /** 426 * Releases this harness's HTTP client and its connection pool. Declares no checked exception, so 427 * callers need not handle one. 428 */ 429 @Override 430 void close(); 431}