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}