Skip to content

feat: Epic FHIR integration with dual authentication#17

Merged
rsalus merged 1 commit intomainfrom
feature/epic-integration
Jan 31, 2026
Merged

feat: Epic FHIR integration with dual authentication#17
rsalus merged 1 commit intomainfrom
feature/epic-integration

Conversation

@rsalus
Copy link
Contributor

@rsalus rsalus commented Jan 29, 2026

Summary

Implements dual OAuth authentication for Epic FHIR R4 API with strategy pattern. CDS Hook requests use the provided fhirAuthorization.access_token, while backend services use JWT-based client credentials flow with token caching.

Changes

  • Token StrategiesCdsHookTokenStrategy extracts tokens from CDS Hook context; JwtBackendTokenStrategy handles JWT generation with RS384 signing and 55-minute token caching
  • Token ResolutionTokenStrategyResolver selects strategy based on request context (CDS Hook priority, JWT fallback)
  • HTTP Client ProviderFhirHttpClientProvider attaches Bearer tokens to authenticated HttpClient instances
  • FHIR Client Refactor — Removed accessToken parameter from all IFhirHttpClient methods; auth handled internally
  • CDS MiddlewareCdsHookTokenMiddleware extracts FHIR tokens from CDS Hook request payloads

Test Plan

Unit tests cover all new components with 46 new tests. Integration verified with full test suite.


Results: Tests 102 ✓ · Build 0 errors
Design: docs/designs/2026-01-28-epic-fhir-integration.md

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR adds comprehensive FHIR authentication infrastructure including token acquisition strategies for CDS Hooks and JWT-based backend flows, a middleware to extract tokens from request payloads, an authenticated HTTP client provider, and extensive test coverage for all new components.

Changes

Cohort / File(s) Summary
Test Suites
apps/gateway/Gateway.API.Tests/Contracts/Http/FhirHttpClientProviderTests.cs, apps/gateway/Gateway.API.Tests/Contracts/Http/TokenAcquisitionStrategyTests.cs, apps/gateway/Gateway.API.Tests/DependencyExtensionsTests.cs, apps/gateway/Gateway.API.Tests/Middleware/CdsHookTokenMiddlewareTests.cs, apps/gateway/Gateway.API.Tests/Services/Http/CdsHookTokenStrategyTests.cs, apps/gateway/Gateway.API.Tests/Services/Http/FhirHttpClientProviderTests.cs, apps/gateway/Gateway.API.Tests/Services/Http/JwtBackendTokenStrategyTests.cs
Added 8 comprehensive test suites with 30+ test methods validating token strategies, middleware token extraction, HTTP client provider creation, dependency injection registration, and JWT generation/caching behavior.
Contracts/Interfaces
apps/gateway/Gateway.API/Contracts/Http/IFhirHttpClientProvider.cs, apps/gateway/Gateway.API/Contracts/Http/ITokenStrategyResolver.cs
Added interfaces defining contracts for authenticated HTTP client provisioning and token strategy resolution.
Middleware
apps/gateway/Gateway.API/Middleware/CdsHookTokenMiddleware.cs
Added middleware extracting FHIR access tokens from CDS Hooks POST request payloads (under /cds-hooks), buffering request streams, and storing tokens in HttpContext.Items for downstream consumption.
Token Strategies
apps/gateway/Gateway.API/Services/Http/CdsHookTokenStrategy.cs, apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs
Added two ITokenAcquisitionStrategy implementations: CdsHookTokenStrategy reads tokens from HttpContext.Items (populated by middleware), JwtBackendTokenStrategy generates JWT client assertions, exchanges them for tokens via OAuth endpoint, and caches tokens for 55 minutes.
HTTP Client Provider
apps/gateway/Gateway.API/Services/Http/FhirHttpClientProvider.cs
Added provider creating authenticated HttpClient instances named "EpicFhir" with Bearer token Authorization headers resolved via ITokenStrategyResolver.
Configuration & Dependencies
apps/gateway/Gateway.API/Configuration/EpicFhirOptions.cs, apps/gateway/Gateway.API/Gateway.API.csproj
Extended EpicFhirOptions with PrivateKeyPath and SigningAlgorithm properties; added InternalsVisibleTo declaration and System.IdentityModel.Tokens.Jwt package reference.

Sequence Diagram(s)

sequenceDiagram
    participant Client as CDS Hooks Client
    participant Middleware as CdsHookTokenMiddleware
    participant HttpContext as HttpContext.Items
    participant Resolver as TokenStrategyResolver
    participant Strategy as ITokenAcquisitionStrategy
    participant Provider as FhirHttpClientProvider
    participant Factory as IHttpClientFactory
    participant Token as TokenEndpoint/Cache

    Client->>Middleware: POST /cds-hooks/...
    Middleware->>Middleware: ExtractAndStoreFhirTokenAsync()
    Middleware->>HttpContext: Items["FhirAccessToken"] = token
    Middleware->>Resolver: Resolve()
    Resolver->>Strategy: Return appropriate strategy
    
    Note over Strategy: CdsHookTokenStrategy or<br/>JwtBackendTokenStrategy
    
    Provider->>Factory: CreateClient("EpicFhir")
    Factory-->>Provider: HttpClient instance
    Provider->>Resolver: Resolve()
    Resolver-->>Provider: Strategy instance
    Provider->>Strategy: AcquireTokenAsync()
    
    alt CdsHookTokenStrategy
        Strategy->>HttpContext: Get Items["FhirAccessToken"]
        HttpContext-->>Strategy: token
    else JwtBackendTokenStrategy
        Strategy->>Strategy: Check cache for token
        alt Token cached
            Strategy-->>Provider: Return cached token
        else Generate & exchange
            Strategy->>Strategy: GenerateClientAssertion()
            Strategy->>Token: POST JWT + grant_type
            Token-->>Strategy: access_token
            Strategy->>Strategy: Cache for 55 min
        end
    end
    
    Strategy-->>Provider: access_token
    Provider->>Provider: Set Authorization: Bearer {token}
    Provider-->>Client: Authenticated HttpClient
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🔐 Tokens flow through CDS Hooks with grace,
JWT signatures in their rightful place,
Middleware guards the FHIR gates,
Strategies resolve what each request awaits,
Authenticated clients stand ready to call!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Epic FHIR integration with dual authentication' clearly and concisely summarizes the main feature addition—Epic FHIR integration with a dual OAuth strategy pattern.
Description check ✅ Passed The description comprehensively covers the changeset, detailing token strategies, token resolution, HTTP client provider, FHIR client refactoring, CDS middleware, and test coverage—all directly related to the PR's objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/epic-integration

Comment @coderabbitai help to get the list of available commands and usage tips.

@rsalus
Copy link
Contributor Author

rsalus commented Jan 29, 2026

issue: Missing guard clauses in nearly all service methods, e.g., ArgumentNullException.ThrowIfNull(object).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API/Middleware/CdsHookTokenMiddleware.cs`:
- Around line 40-72: The ExtractAndStoreFhirTokenAsync method currently assumes
the JSON root is an object and calls doc.RootElement.TryGetProperty which will
throw for non-object roots; update the method to check doc.RootElement.ValueKind
== JsonValueKind.Object (or otherwise guard) before calling TryGetProperty on
the root element, and only attempt to access "fhirAuthorization" and
"access_token" when the root is an object—keep existing JsonException handling
and the context.Items["FhirAccessToken"] assignment/logging behavior.

In `@apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs`:
- Around line 85-101: The ExchangeJwtForTokenAsync method uses an unnamed
HttpClient with default timeout and assumes the response JSON contains
"access_token"; fix by using a named/typed client from _httpClientFactory
configured with an explicit timeout for the token exchange (or create a
CancellationTokenSource with a short timeout around the PostAsync call) and by
defensively parsing the JSON: after reading the JsonElement check
json.TryGetProperty("access_token") and return null or throw a clear,
descriptive exception if missing (include context like _options.TokenEndpoint
and the response body when logging), and wrap the HTTP call/parse in a try/catch
to convert transient/parse failures into meaningful exceptions.
- Around line 112-118: The aud claim is set to an empty string when
_options.TokenEndpoint is null, producing an invalid JWT; update
JwtBackendTokenStrategy to validate _options.TokenEndpoint early (e.g., in the
constructor or at the start of the method that builds the claims) and throw a
clear exception (ArgumentException/InvalidOperationException) if TokenEndpoint
is null or empty, then use the validated TokenEndpoint when creating the claims
array (the Claim with JwtRegisteredClaimNames.Aud) so the aud is never an empty
string.
- Around line 137-148: LoadPrivateKey currently calls RSA.Create() and returns
an RSA that is never disposed; change LoadPrivateKey to avoid leaking by
exporting the key parameters from the PEM-loaded RSA, disposing that temporary
RSA, and returning a RsaSecurityKey constructed from a new RSA instance
initialized with rsa.ExportParameters(true) (i.e., create tempRsa =
RSA.Create(); tempRsa.ImportFromPem(...); var parms =
tempRsa.ExportParameters(true); tempRsa.Dispose(); var rsaForKey = RSA.Create();
rsaForKey.ImportParameters(parms); return new RsaSecurityKey(rsaForKey)). This
keeps the final RSA owned where RsaSecurityKey expects it while ensuring the
initial PEM loader is disposed and prevents the per-cache-miss leak; update any
callers expecting RSA (e.g., code that called LoadPrivateKey and wrapped it in
RsaSecurityKey) to use the new RsaSecurityKey return or adapt accordingly.
🧹 Nitpick comments (16)
apps/gateway/Gateway.API.Tests/Contracts/Http/TokenStrategyResolverTests.cs (1)

6-22: Test doesn’t exercise the real resolver.
Right now it only verifies a mocked return value. Consider instantiating the concrete TokenStrategyResolver with real strategies and asserting selection behavior (e.g., CDS Hook vs JWT).

apps/gateway/Gateway.API/Services/Http/TokenStrategyResolver.cs (1)

10-24: Consider using interfaces instead of concrete strategy types.

The constructor accepts CdsHookTokenStrategy and JwtBackendTokenStrategy directly rather than ITokenAcquisitionStrategy. This couples the resolver to specific implementations and makes isolated unit testing harder.

If extending with additional strategies is anticipated, consider:

♻️ Interface-based injection
-    private readonly CdsHookTokenStrategy _cdsStrategy;
-    private readonly JwtBackendTokenStrategy _jwtStrategy;
+    private readonly ITokenAcquisitionStrategy _cdsStrategy;
+    private readonly ITokenAcquisitionStrategy _jwtStrategy;

     public TokenStrategyResolver(
-        CdsHookTokenStrategy cdsStrategy,
-        JwtBackendTokenStrategy jwtStrategy)
+        [FromKeyedServices("CdsHook")] ITokenAcquisitionStrategy cdsStrategy,
+        [FromKeyedServices("JwtBackend")] ITokenAcquisitionStrategy jwtStrategy)

That said, if these are the only two strategies and the current DI setup works, the pragmatic concrete approach is acceptable.

apps/gateway/Gateway.API/Services/Http/FhirHttpClientProvider.cs (1)

39-49: Method name "GetAuthenticatedClientAsync" is misleading—it may return a client without authentication.

The provider intentionally returns an unauthenticated client when token acquisition fails, relying on callers to handle the resulting 401 responses. While error handling exists downstream (FhirHttpClient and EpicFhirContext both check for HttpStatusCode.Unauthorized), the deferred failure point obscures the intent.

Consider either: (1) renaming to clarify the behavior (e.g., GetClientAsync), (2) documenting that callers must handle potential 401s, or (3) throwing to fail fast if authentication is mandatory for all FHIR operations.

apps/gateway/Gateway.API/Services/FhirDataAggregator.cs (1)

35-39: Consider adding a guard clause for patientId.

Per the coding guidelines favoring guard clauses first, validating patientId at the entry point would provide clearer error messages rather than propagating failures to individual FHIR calls.

💡 Optional guard clause
 public async Task<ClinicalBundle> AggregateClinicalDataAsync(
     string patientId,
     CancellationToken cancellationToken = default)
 {
+    ArgumentException.ThrowIfNullOrWhiteSpace(patientId);
+
     _logger.LogInformation("Aggregating clinical data for patient {PatientId}", patientId);
apps/gateway/Gateway.API.Tests/Services/Http/CdsHookTokenStrategyTests.cs (1)

47-65: Consider adding a test for when FhirAccessToken exists but is null or non-string.

Edge case: what happens if middleware stores a non-string or null value under the key? This would help ensure the as string cast in the production code handles edge cases gracefully.

💡 Optional edge case test
[Test]
public async Task CdsHookTokenStrategy_AcquireTokenAsync_ReturnsNullWhenTokenIsNotString()
{
    // Arrange
    var httpContext = new DefaultHttpContext();
    httpContext.Items["FhirAccessToken"] = 12345; // Non-string value

    var httpContextAccessor = Substitute.For<IHttpContextAccessor>();
    httpContextAccessor.HttpContext.Returns(httpContext);

    var strategy = new CdsHookTokenStrategy(httpContextAccessor);

    // Act
    var token = await strategy.AcquireTokenAsync();

    // Assert
    await Assert.That(token).IsNull();
}
apps/gateway/Gateway.API.Tests/Contracts/Http/FhirHttpClientProviderTests.cs (2)

6-42: These tests mock the interface rather than testing the implementation.

Both tests substitute IFhirHttpClientProvider and verify mock behavior. This tests NSubstitute's setup, not FhirHttpClientProvider's actual logic. Consider testing the real implementation with mocked dependencies (like ITokenStrategyResolver and IHttpClientFactory) to validate actual authentication flow.


13-13: HttpClient instances should be disposed.

HttpClient implements IDisposable. While not critical in tests, proper disposal is good hygiene.

💡 Dispose HttpClient in tests
 [Test]
 public async Task IFhirHttpClientProvider_GetAuthenticatedClientAsync_ReturnsHttpClient()
 {
     // Arrange
     var provider = Substitute.For<IFhirHttpClientProvider>();
-    var expectedClient = new HttpClient();
+    using var expectedClient = new HttpClient();
     provider.GetAuthenticatedClientAsync(Arg.Any<CancellationToken>())
         .Returns(Task.FromResult(expectedClient));

Also applies to: 31-31

apps/gateway/Gateway.API/Services/Http/CdsHookTokenStrategy.cs (2)

17-20: Add a guard clause for the constructor parameter.

Per coding guidelines requiring guard clauses first, validate httpContextAccessor to fail fast with a clear error.

♻️ Proposed fix
 public CdsHookTokenStrategy(IHttpContextAccessor httpContextAccessor)
 {
-    _httpContextAccessor = httpContextAccessor;
+    _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
 }

23-24: Consider extracting the magic string to a constant.

"FhirAccessToken" is used in both CanHandle and AcquireTokenAsync. A shared constant would reduce duplication and prevent typo-related bugs.

💡 Extract constant
 public sealed class CdsHookTokenStrategy : ITokenAcquisitionStrategy
 {
+    private const string FhirAccessTokenKey = "FhirAccessToken";
     private readonly IHttpContextAccessor _httpContextAccessor;

     // ...

     public bool CanHandle =>
-        _httpContextAccessor.HttpContext?.Items.ContainsKey("FhirAccessToken") == true;
+        _httpContextAccessor.HttpContext?.Items.ContainsKey(FhirAccessTokenKey) == true;

     public Task<string?> AcquireTokenAsync(CancellationToken ct = default)
     {
-        var token = _httpContextAccessor.HttpContext?.Items["FhirAccessToken"] as string;
+        var token = _httpContextAccessor.HttpContext?.Items[FhirAccessTokenKey] as string;
         return Task.FromResult(token);
     }
 }

Also applies to: 29-29

apps/gateway/Gateway.API.Tests/Services/Http/FhirHttpClientProviderTests.cs (1)

27-44: HttpClient instances should be disposed in tests.

The HttpClient instances created here (and in other tests at lines 50, 70, 90) are not disposed. While this typically works fine in short-lived test processes, it's cleaner to use using statements or dispose in teardown.

♻️ Optional: Wrap in using statement
     [Test]
     public async Task FhirHttpClientProvider_GetAuthenticatedClientAsync_UsesHttpClientFactory()
     {
         // Arrange
-        var expectedClient = new HttpClient();
+        using var expectedClient = new HttpClient();
         _httpClientFactory.CreateClient("EpicFhir").Returns(expectedClient);
apps/gateway/Gateway.API/DependencyExtensions.cs (2)

68-71: ValidateDataAnnotations() has no effect without data annotation attributes.

EpicFhirOptions uses C# required keyword, not [Required] attributes. ValidateDataAnnotations() won't validate anything here. The manual check at lines 73-77 partially compensates, but only for FhirBaseUrl.

Consider either:

  1. Adding [Required] attributes to EpicFhirOptions properties, or
  2. Using a custom validation delegate with .Validate()
♻️ Option: Use Validate() with delegate
         services.AddOptions<EpicFhirOptions>()
             .Bind(configuration.GetSection(EpicFhirOptions.SectionName))
-            .ValidateDataAnnotations()
+            .Validate(o => !string.IsNullOrWhiteSpace(o.FhirBaseUrl) && !string.IsNullOrWhiteSpace(o.ClientId),
+                "EpicFhirOptions requires FhirBaseUrl and ClientId")
             .ValidateOnStart();
-
-        var fhirBaseUrl = configuration["Epic:FhirBaseUrl"];
-        if (string.IsNullOrWhiteSpace(fhirBaseUrl))
-        {
-            throw new InvalidOperationException("Epic:FhirBaseUrl must be configured.");
-        }

107-122: Consider documenting the relationship between AddFhirClients and AddEpicFhirServices.

AddFhirClients calls AddEpicFhirServices internally. If both are called separately, some services get registered twice. While this works, a /// <remarks> noting that AddFhirClients includes AddEpicFhirServices would prevent confusion.

apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs (1)

144-147: Synchronous file I/O in async context.

File.ReadAllText blocks a thread pool thread. Consider File.ReadAllTextAsync or loading the key once at startup. Given this only runs on cache miss (~every 55 min), impact is low but avoidable.

♻️ Async alternative
-    private RSA LoadPrivateKey()
+    private async Task<RSA> LoadPrivateKeyAsync()
     {
         if (string.IsNullOrEmpty(_options.PrivateKeyPath))
         {
             throw new InvalidOperationException("PrivateKeyPath is not configured");
         }

         var rsa = RSA.Create();
-        var keyPem = File.ReadAllText(_options.PrivateKeyPath);
+        var keyPem = await File.ReadAllTextAsync(_options.PrivateKeyPath);
         rsa.ImportFromPem(keyPem);
         return rsa;
     }

This would require GenerateClientAssertion to become async as well.

apps/gateway/Gateway.API.Tests/DependencyExtensionsTests.cs (1)

121-142: Consider disposing ServiceProvider in tests.

The ServiceProvider built at line 133 (and in other tests) is disposable. While typically fine in tests, adding using or explicit disposal is cleaner practice.

♻️ Optional: Use using declaration
         // Act
         services.AddEpicFhirServices(configuration);
-        var provider = services.BuildServiceProvider();
+        using var provider = services.BuildServiceProvider();
apps/gateway/Gateway.API/Services/Fhir/EpicFhirContext.cs (1)

15-31: Class should be sealed per coding guidelines.

The coding guidelines specify "Sealed by default" for C# classes. Unless inheritance is explicitly needed, consider sealing EpicFhirContext<TResource>.

♻️ Proposed change
-public class EpicFhirContext<TResource> : IFhirContext<TResource> where TResource : class
+public sealed class EpicFhirContext<TResource> : IFhirContext<TResource> where TResource : class

As per coding guidelines: "Sealed by default".

apps/gateway/Gateway.API.Tests/Services/Http/JwtBackendTokenStrategyTests.cs (1)

15-15: Consider sealing the test class.

The coding guidelines recommend "sealed by default." While this is less critical for test classes, marking it sealed maintains consistency with production code patterns.

-public class JwtBackendTokenStrategyTests
+public sealed class JwtBackendTokenStrategyTests

@rsalus
Copy link
Contributor Author

rsalus commented Jan 29, 2026

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
apps/gateway/Gateway.API/Services/DocumentUploader.cs (2)

24-32: Add guard clauses to validate constructor parameters.

Per coding guidelines requiring guard clauses first, constructor parameters should be validated. This was also flagged by the PR author.

🛡️ Proposed fix
 public DocumentUploader(
     IFhirHttpClient fhirHttpClient,
     ILogger<DocumentUploader> logger,
     IOptions<DocumentOptions> options)
 {
+    ArgumentNullException.ThrowIfNull(fhirHttpClient);
+    ArgumentNullException.ThrowIfNull(logger);
+    ArgumentNullException.ThrowIfNull(options);
     _fhirHttpClient = fhirHttpClient;
     _logger = logger;
     _options = options.Value;
 }

35-48: Add guard clauses for method parameters.

pdfBytes and patientId are required for document upload to succeed; validate them early.

🛡️ Proposed fix
 public async Task<Result<string>> UploadDocumentAsync(
     byte[] pdfBytes,
     string patientId,
     string? encounterId,
     CancellationToken cancellationToken = default)
 {
+    ArgumentNullException.ThrowIfNull(pdfBytes);
+    ArgumentException.ThrowIfNullOrWhiteSpace(patientId);
+
     _logger.LogInformation(
         "Uploading PA form. Size={Size} bytes",
         pdfBytes.Length);
apps/gateway/Gateway.API/Services/FhirDataAggregator.cs (2)

24-32: Missing guard clauses for constructor parameters.

Constructor dependencies should be validated. This aligns with the PR author's own observation about missing guard clauses.

Proposed fix
 public FhirDataAggregator(
     IFhirClient fhirClient,
     IOptions<ClinicalQueryOptions> options,
     ILogger<FhirDataAggregator> logger)
 {
+    ArgumentNullException.ThrowIfNull(fhirClient);
+    ArgumentNullException.ThrowIfNull(options);
+    ArgumentNullException.ThrowIfNull(logger);
     _fhirClient = fhirClient;
     _options = options.Value;
     _logger = logger;
 }

35-37: Missing guard clause for patientId parameter.

Public methods should validate input parameters early. An empty or null patientId would propagate to multiple downstream calls before failing.

Proposed fix
 public async Task<ClinicalBundle> AggregateClinicalDataAsync(
     string patientId,
     CancellationToken cancellationToken = default)
 {
+    ArgumentException.ThrowIfNullOrWhiteSpace(patientId);
     _logger.LogInformation("Aggregating clinical data for patient {PatientId}", patientId);
apps/gateway/Gateway.API/Services/Fhir/BaseFhirRepository.cs (2)

28-32: Missing guard clauses in constructor.

As per coding guidelines, guard clauses should come first. The constructor doesn't validate context or logger, which could lead to NullReferenceException later.

🛡️ Proposed fix
     protected BaseFhirRepository(IFhirContext<TResource> context, ILogger logger)
     {
+        ArgumentNullException.ThrowIfNull(context);
+        ArgumentNullException.ThrowIfNull(logger);
         Context = context;
         Logger = logger;
     }

80-83: Derived class constructor also missing guard clauses.

BaseFhirRepositoryWithDateRange constructor should validate parameters before passing to base.

🛡️ Proposed fix
     protected BaseFhirRepositoryWithDateRange(IFhirContext<TResource> context, ILogger logger)
         : base(context, logger)
     {
+        // Base constructor validates; nothing additional needed here
     }

Actually, since the base constructor now validates, this is fine. However, FindByPatientSinceAsync should validate patientId:

     public virtual Task<Result<IReadOnlyList<TResource>>> FindByPatientSinceAsync(
         string patientId,
         DateOnly since,
         CancellationToken ct = default)
     {
+        ArgumentException.ThrowIfNullOrEmpty(patientId);
         var query = BuildQuery(
             ("patient", patientId),
             (DateFieldName, $"ge{since:yyyy-MM-dd}"));
apps/gateway/Gateway.API/Services/FhirClient.cs (1)

29-121: Add guard clauses to validate patientId and documentId before building FHIR requests.

Empty or whitespace-only IDs will be interpolated directly into URLs and query strings (e.g., Patient/ or patient=&clinical-status=active), creating malformed requests that fail at the HTTP layer unnecessarily. Add early validation in each public method to fail fast with clear error messages.

🔧 Proposed guard clause pattern
 public async Task<PatientInfo?> GetPatientAsync(
     string patientId,
     CancellationToken cancellationToken = default)
 {
+    if (string.IsNullOrWhiteSpace(patientId))
+    {
+        throw new ArgumentException("Patient ID is required.", nameof(patientId));
+    }
     var result = await _httpClient.ReadAsync("Patient", patientId, cancellationToken);

Apply the same pattern to all public methods: SearchConditionsAsync, SearchObservationsAsync, SearchProceduresAsync, SearchDocumentsAsync, and GetDocumentContentAsync.

Per coding guidelines: "Control: Guard clauses first, early return, no arrow code".

🤖 Fix all issues with AI agents
In
`@apps/gateway/Gateway.API.Tests/Services/Http/JwtBackendTokenStrategyTests.cs`:
- Around line 203-204: The test currently asserts
_httpClientFactory.DidNotReceive().CreateClient(Arg.Any<string>()) but
JwtBackendTokenStrategy uses the parameterless overload CreateClient() (see
JwtBackendTokenStrategy's CreateClient() usage), so update the verification to
assert the parameterless CreateClient() was not called (e.g.,
_httpClientFactory.DidNotReceive().CreateClient()) to correctly reflect the
implementation and ensure no HTTP client was created; locate the assertion in
JwtBackendTokenStrategyTests (symbol: _httpClientFactory and the CreateClient
call) and replace the overload check accordingly.

In `@apps/gateway/Gateway.API/Configuration/EpicAuthOptions.cs`:
- Around line 1-42: Delete the unused EpicAuthOptions class (remove the
EpicAuthOptions type declaration) since EpicFhirOptions is the active config
type; then update EpicFhirOptionsTests.cs to remove any tests or references that
reference EpicAuthOptions and adjust those tests to only construct/bind/assert
EpicFhirOptions (ensure binding still targets the "Epic" section and that test
assertions cover the same properties: FhirBaseUrl, ClientId, ClientSecret,
TokenEndpoint, PrivateKeyPath, SigningAlgorithm). Make sure no other files
reference EpicAuthOptions after deletion.

In `@apps/gateway/Gateway.API/Configuration/EpicFhirOptions.cs`:
- Around line 33-41: Add DataAnnotations to EpicFhirOptions: mark the
PrivateKeyPath property with [Required] and, if a FileExists validation
attribute is available in the project, add [FileExists] as well so
missing/invalid files fail startup; mark SigningAlgorithm with
[RegularExpression("^(RS256|RS384|RS512)$")] to allow only those values (remove
the silent fallback). Ensure the class imports the DataAnnotations namespace and
keep the existing default value for SigningAlgorithm if you still want it but
let validation catch invalid overrides.

In `@apps/gateway/Gateway.API/DependencyExtensions.cs`:
- Around line 65-71: Add guard clauses at the top of AddEpicFhirServices to
validate the incoming parameters: throw ArgumentNullException (or use
ArgumentNullException.ThrowIfNull) for both services and configuration before
any processing; update the method signature body (in AddEpicFhirServices) so the
null checks run first and then proceed to the existing options binding and
validation calls.

In `@apps/gateway/Gateway.API/Gateway.API.csproj`:
- Around line 32-34: Update the PackageReference for
System.IdentityModel.Tokens.Jwt in Gateway.API.csproj from Version="8.2.1" to
the latest stable (e.g., "8.15.0"); after changing the Version attribute on the
PackageReference element (System.IdentityModel.Tokens.Jwt) restore/rehydrate
packages and run unit/integration tests, then smoke-test token handling, logging
assertions (log sanitization changes), ConfigurationManager refresh behavior,
and any code using experimental validation APIs to ensure no runtime/behavioral
regressions.

In `@apps/gateway/Gateway.API/Middleware/CdsHookTokenMiddleware.cs`:
- Around line 18-22: Constructor CdsHookTokenMiddleware lacks null checks for
its dependencies; add guard clauses at the start of the
CdsHookTokenMiddleware(RequestDelegate next, ILogger<CdsHookTokenMiddleware>
logger) constructor to validate that next and logger are not null and throw
ArgumentNullException using nameof(next) and nameof(logger) if they are, then
assign to the fields _next and _logger as before.

In `@apps/gateway/Gateway.API/Services/Fhir/BaseFhirRepository.cs`:
- Around line 43-49: The FindByPatientAsync method lacks a guard for the
patientId parameter; add a validation at the top of
BaseFhirRepository.FindByPatientAsync to check for null, empty, or whitespace
(e.g., string.IsNullOrWhiteSpace(patientId)) and throw an
ArgumentException/ArgumentNullException with nameof(patientId) if invalid,
before constructing the query and calling Context.SearchAsync(query, ct).
- Around line 35-40: Add a guard clause at the start of GetByIdAsync to validate
the id parameter (check for null or empty/whitespace) and fail fast instead of
calling Context.ReadAsync with an invalid id; specifically, inside GetByIdAsync
perform a check like string.IsNullOrWhiteSpace(id) and throw an appropriate
exception (ArgumentNullException or ArgumentException with nameof(id)) when
invalid so callers receive a clear error before delegating to Context.ReadAsync.

In `@apps/gateway/Gateway.API/Services/Fhir/EpicFhirContext.cs`:
- Around line 26-31: The constructor EpicFhirContext(IFhirHttpClientProvider
clientProvider, ILogger<EpicFhirContext<TResource>> logger) lacks null-argument
guard clauses; add early-throw checks at the top of the constructor for
clientProvider and logger (throw ArgumentNullException with the parameter name)
before assigning to _clientProvider and _logger, then set _resourceType =
typeof(TResource).Name as before so guards come first and prevent
arrow-style/late-failure initialization.

In `@apps/gateway/Gateway.API/Services/Fhir/FhirHttpClient.cs`:
- Around line 25-29: Add guard clauses to the FhirHttpClient constructor to
validate inputs: check that clientProvider and logger are not null and throw
ArgumentNullException for each (e.g., in the FhirHttpClient constructor before
assigning _clientProvider and _logger) so the class fails fast on
misconfiguration; keep the checks at the top of the constructor and then assign
the fields.

In `@apps/gateway/Gateway.API/Services/FhirClient.cs`:
- Around line 167-224: The mappers currently generate synthetic IDs with
Guid.NewGuid() which hides invalid FHIR resources; update MapToCondition,
MapToObservation, MapToProcedure to return null when the "id" property is
missing or not a valid string (remove Guid.NewGuid() fallback) and change
MapToDocument signature to return DocumentInfo? and similarly return null if
"id" is absent; ensure you locate the null-check logic in
MapToCondition/MapToObservation/MapToProcedure (Id =
resource.TryGetProperty("id", out var id) ? ...) and in MapToDocument (docId =
...), replace those fallbacks with an early null return so downstream
null-handling is used consistently.
- Around line 123-165: Add a defensive check before calling EnumerateArray in
SearchAndMapAsync: verify that the JsonElement entries has ValueKind ==
JsonValueKind.Array and if not, log a warning (include resourceType and perhaps
entries.ValueKind) and return the empty results to avoid
InvalidOperationException; apply the identical guard to the analogous iteration
in EpicFhirContext (the same bundle/entry handling method) so both places
validate entries.ValueKind before EnumerateArray().

In `@apps/gateway/Gateway.API/Services/Http/CdsHookTokenStrategy.cs`:
- Around line 17-20: The CdsHookTokenStrategy constructor is missing a null
guard for the injected IHttpContextAccessor; update the
CdsHookTokenStrategy(IHttpContextAccessor httpContextAccessor) constructor to
validate httpContextAccessor and throw an ArgumentNullException (or use
ArgumentNullException.ThrowIfNull) before assigning to the _httpContextAccessor
field so the dependency is enforced at construction time.

In `@apps/gateway/Gateway.API/Services/Http/FhirHttpClientProvider.cs`:
- Around line 21-29: The constructor of FhirHttpClientProvider lacks guard
clauses for its dependencies; add ArgumentNullException.ThrowIfNull checks for
httpClientFactory, tokenResolver, and logger at the start of the
FhirHttpClientProvider constructor so the parameters IHttpClientFactory
httpClientFactory, ITokenStrategyResolver tokenResolver, and
ILogger<FhirHttpClientProvider> logger are validated before assignment to the
_httpClientFactory, _tokenResolver, and _logger fields.

In `@apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs`:
- Around line 33-43: Add null-check guard clauses to the JwtBackendTokenStrategy
constructor: validate httpClientFactory, options, tokenCache, and logger and
throw ArgumentNullException for any that are null before assigning to the
private fields. Use the parameter names (httpClientFactory, options, tokenCache,
logger) in the exceptions and dereference options.Value only after options is
validated.

In `@apps/gateway/Gateway.API/Services/Http/TokenStrategyResolver.cs`:
- Around line 18-24: The TokenStrategyResolver constructor lacks null checks for
its dependencies; add guard clauses at the top of the constructor to validate
that parameters cdsStrategy and jwtStrategy are not null and throw appropriate
ArgumentNullException (or similar) before assigning to _cdsStrategy and
_jwtStrategy so the constructor fails fast if dependencies are missing.
🧹 Nitpick comments (18)
apps/gateway/Gateway.API/Contracts/Fhir/IFhirRepository.cs (1)

9-45: Two public types in one file—consider splitting if strict adherence to guidelines is required.

The coding guidelines specify "one public type per file," but IFhirRepository<TResource> and IFhirRepositoryWithDateRange<TResource> are tightly coupled (inheritance relationship, same bounded context). Keeping them together is pragmatic and improves discoverability.

If you want to align strictly with the guideline, extract IFhirRepositoryWithDateRange<TResource> to its own file. Otherwise, this is a reasonable exception.

As per coding guidelines: apps/gateway/**/*.cs: SOLID: One public type per file.

apps/gateway/Gateway.API/Models/AnalysisResponses.cs (1)

6-127: Consider splitting public records into separate files. This file hosts five public types, which makes navigation and ownership harder as the API surface grows.

As per coding guidelines: SOLID: One public type per file.

apps/gateway/Gateway.API.Tests/Services/Http/TokenStrategyResolverTests.cs (1)

42-57: Consider whether async is needed.

TokenStrategyResolver.Resolve() is synchronous (returns ITokenAcquisitionStrategy directly). The test method is marked async Task but only awaits the TUnit assertion. While TUnit's Assert.That() is awaitable, using async for a synchronous SUT operation is slightly misleading.

♻️ Optional: Simplify to sync test if TUnit supports it

If TUnit supports synchronous assertions, consider:

 [Test]
-public async Task TokenStrategyResolver_Resolve_ReturnsCdsStrategyWhenTokenPresent()
+public void TokenStrategyResolver_Resolve_ReturnsCdsStrategyWhenTokenPresent()
 {
     // ...
     var result = sut.Resolve();

-    await Assert.That(result).IsSameReferenceAs(_cdsStrategy);
+    Assert.That(result).IsSameReferenceAs(_cdsStrategy);
 }

Otherwise, the current approach works fine with TUnit's async assertions.

apps/gateway/Gateway.API.Tests/Services/Fhir/FhirHttpClientTests.cs (2)

19-28: Remove unused _mockHttpClient field.

The _mockHttpClient field is created in the constructor but never used—each test creates its own HttpClient with a specific MockHttpMessageHandler.

♻️ Proposed fix
 public class FhirHttpClientTests
 {
     private readonly IFhirHttpClientProvider _clientProvider;
     private readonly ILogger<FhirHttpClient> _logger;
     private readonly FhirHttpClient _sut;
-    private readonly HttpClient _mockHttpClient;

     public FhirHttpClientTests()
     {
         _clientProvider = Substitute.For<IFhirHttpClientProvider>();
         _logger = Substitute.For<ILogger<FhirHttpClient>>();
-        _mockHttpClient = CreateMockHttpClient();

         _sut = new FhirHttpClient(_clientProvider, _logger);
     }

Also remove the unused helper method at lines 202-205.


211-226: Seal MockHttpMessageHandler per coding guidelines.

The coding guidelines specify "sealed by default" for types.

♻️ Proposed fix
-internal class MockHttpMessageHandler : HttpMessageHandler
+internal sealed class MockHttpMessageHandler : HttpMessageHandler
apps/gateway/Gateway.API/Configuration/EpicAuthOptions.cs (1)

6-42: Consider using record for configuration DTOs.

Per coding guidelines, records are preferred for DTOs. Since this is a pure data container with init-only properties:

♻️ Proposed change
-public sealed class EpicAuthOptions
+public sealed record EpicAuthOptions

Records provide value equality and concise syntax for immutable data.

apps/gateway/Gateway.API.Tests/Contracts/Http/TokenStrategyResolverTests.cs (1)

8-22: Test validates mock behavior, not actual resolver logic.

This test mocks ITokenStrategyResolver itself, so it only confirms that NSubstitute returns configured values. Consider testing the concrete TokenStrategyResolver class to verify:

  • Returns CDS strategy when _cdsStrategy.CanHandle is true
  • Falls back to JWT strategy when CDS cannot handle
💡 Suggested approach
[Test]
public async Task TokenStrategyResolver_Resolve_ReturnsCdsStrategyWhenCanHandle()
{
    // Arrange
    var cdsStrategy = Substitute.For<ITokenAcquisitionStrategy>();
    cdsStrategy.CanHandle.Returns(true);
    var jwtStrategy = Substitute.For<ITokenAcquisitionStrategy>();
    var resolver = new TokenStrategyResolver(cdsStrategy, jwtStrategy);

    // Act
    var result = resolver.Resolve();

    // Assert
    await Assert.That(result).IsSameReferenceAs(cdsStrategy);
}
apps/gateway/Gateway.API.Tests/Contracts/Http/TokenAcquisitionStrategyTests.cs (1)

6-36: Interface mock tests have limited value.

These tests verify that NSubstitute can mock the interface, not the behavior of actual implementations. Given that CdsHookTokenStrategyTests and JwtBackendTokenStrategyTests exist elsewhere in the PR, consider whether these mock-verification tests add meaningful coverage.

If the intent is contract testing to ensure the interface shape is mockable, that's valid but worth documenting in the test class.

apps/gateway/Gateway.API/Services/Http/TokenStrategyResolver.cs (1)

10-11: Constructor depends on concrete strategy types instead of interfaces.

Injecting CdsHookTokenStrategy and JwtBackendTokenStrategy directly couples this resolver to concrete implementations. Using ITokenAcquisitionStrategy would improve testability and align with DIP. However, since you need to distinguish them for priority logic, consider a marker interface or named registration pattern.

Also applies to: 18-20

apps/gateway/Gateway.API.Tests/Contracts/Http/FhirHttpClientProviderTests.cs (3)

6-23: Tests verify mock contract, not implementation behavior.

These tests mock IFhirHttpClientProvider and verify the mock returns what it was configured to return. This validates the interface signature but doesn't exercise FhirHttpClientProvider's actual logic (token acquisition, client configuration).

Consider adding tests for the concrete FhirHttpClientProvider class that verify:

  • Token strategy resolution and application
  • HttpClient configuration (base address, headers)
  • Error handling when token acquisition fails

13-13: HttpClient is not disposed.

HttpClient implements IDisposable. While the impact is minimal in tests, disposing resources is good hygiene.

♻️ Suggested fix
     [Test]
     public async Task IFhirHttpClientProvider_GetAuthenticatedClientAsync_ReturnsHttpClient()
     {
         // Arrange
         var provider = Substitute.For<IFhirHttpClientProvider>();
-        var expectedClient = new HttpClient();
+        using var expectedClient = new HttpClient();
         provider.GetAuthenticatedClientAsync(Arg.Any<CancellationToken>())
             .Returns(Task.FromResult(expectedClient));

30-31: Disposable resources not disposed.

Both CancellationTokenSource and HttpClient should be disposed.

♻️ Suggested fix
         // Arrange
         var provider = Substitute.For<IFhirHttpClientProvider>();
-        var cts = new CancellationTokenSource();
-        var expectedClient = new HttpClient();
+        using var cts = new CancellationTokenSource();
+        using var expectedClient = new HttpClient();
apps/gateway/Gateway.API/Services/Http/CdsHookTokenStrategy.cs (1)

23-24: Magic string "FhirAccessToken" duplicated across files.

This key is used here and in CdsHookTokenMiddleware. Consider extracting to a shared constant to prevent typo-induced bugs.

♻️ Example
// In a shared location, e.g., Gateway.API/Constants/HttpContextKeys.cs
public static class HttpContextKeys
{
    public const string FhirAccessToken = "FhirAccessToken";
}

Also applies to: 29-29

apps/gateway/Gateway.API.Tests/DependencyExtensionsTests.cs (1)

48-48: ServiceProvider instances not disposed.

BuildServiceProvider() returns an IDisposable. Not disposing can lead to resource leaks, especially with repeated test runs. This pattern repeats across all tests in this file.

♻️ Suggested pattern
         // Act
         services.AddEpicFhirServices(configuration);
-        var provider = services.BuildServiceProvider();
+        using var provider = services.BuildServiceProvider();

         // Assert

Apply this using pattern to all BuildServiceProvider() calls in this file.

apps/gateway/Gateway.API/DependencyExtensions.cs (1)

107-111: Consider idempotency for AddEpicFhirServices.

AddFhirClients unconditionally calls AddEpicFhirServices. If a consumer calls both methods, or calls AddEpicFhirServices before AddFhirClients, the strategies and resolver will be registered multiple times. Consider using TryAdd* variants or a guard pattern to ensure idempotent registration.

♻️ One approach using TryAddScoped
-        // Token strategies (concrete types needed for TokenStrategyResolver)
-        services.AddScoped<CdsHookTokenStrategy>();
-        services.AddScoped<JwtBackendTokenStrategy>();
-
-        // Token strategy resolver
-        services.AddScoped<ITokenStrategyResolver, TokenStrategyResolver>();
-
-        // FHIR HTTP client provider
-        services.AddScoped<IFhirHttpClientProvider, FhirHttpClientProvider>();
+        // Token strategies (concrete types needed for TokenStrategyResolver)
+        services.TryAddScoped<CdsHookTokenStrategy>();
+        services.TryAddScoped<JwtBackendTokenStrategy>();
+
+        // Token strategy resolver
+        services.TryAddScoped<ITokenStrategyResolver, TokenStrategyResolver>();
+
+        // FHIR HTTP client provider
+        services.TryAddScoped<IFhirHttpClientProvider, FhirHttpClientProvider>();
apps/gateway/Gateway.API/Services/Fhir/JsonElementExtensions.cs (1)

62-65: Empty string handling in given name concatenation.

When GetString() returns null, it's replaced with an empty string, which after string.Join(" ", ...) could produce trailing/leading spaces or double spaces (e.g., "John " if there's a null in the array). Consider filtering nulls/empty values:

♻️ Filter empty values
             var givenNames = given.EnumerateArray()
-                .Select(g => g.GetString() ?? "")
+                .Select(g => g.GetString())
+                .Where(g => !string.IsNullOrEmpty(g))
                 .ToList();
-            return string.Join(" ", givenNames);
+            return givenNames.Count > 0 ? string.Join(" ", givenNames) : null;
apps/gateway/Gateway.API/Services/Fhir/EpicFhirContext.cs (1)

97-102: Add JsonException handling for consistency with FhirHttpClient.

ReadFromJsonAsync can throw JsonException if the response body is malformed JSON. FhirHttpClient catches this, but EpicFhirContext methods do not. Consider adding consistent error handling.

♻️ Add JsonException catch block
         catch (HttpRequestException ex)
         {
             _logger.LogError(ex, "Network error searching {ResourceType}", _resourceType);
             return Result<IReadOnlyList<TResource>>.Failure(FhirError.Network(ex.Message, ex));
         }
+        catch (JsonException ex)
+        {
+            _logger.LogError(ex, "Invalid JSON response searching {ResourceType}", _resourceType);
+            return Result<IReadOnlyList<TResource>>.Failure(
+                FhirError.Validation($"Invalid JSON response: {ex.Message}"));
+        }
apps/gateway/Gateway.API.Tests/Middleware/CdsHookTokenMiddlewareTests.cs (1)

14-21: Consider resetting _nextCalled before each test.

The _nextCalled field is set in the constructor, which runs once per test class instance. If the test framework reuses instances, tests could interfere. Adding a setup method or resetting in each test would be safer.

♻️ Add explicit reset
+    [Before(Test)]
+    public void Setup()
+    {
+        _nextCalled = false;
+    }

Or reset at the start of CreateNextDelegate():

     private RequestDelegate CreateNextDelegate()
     {
+        _nextCalled = false;
         return _ =>
         {
             _nextCalled = true;
             return Task.CompletedTask;
         };
     }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/Gateway.API/Services/FhirClient.cs (1)

29-52: Add guard clauses for string ID parameters before HTTP delegation.

Methods GetPatientAsync (line 29), GetDocumentContentAsync (line 106), and all Search* methods (lines 56–102) accept string IDs without null/empty validation. A null or whitespace patientId/documentId will produce malformed HTTP requests like Patient/ or Binary/. Add guard clauses to validate inputs early and provide clear logging before delegating to IFhirHttpClient.

🔧 Suggested fix
public async Task<PatientInfo?> GetPatientAsync(
    string patientId,
    CancellationToken cancellationToken = default)
{
+    if (string.IsNullOrWhiteSpace(patientId))
+    {
+        _logger.LogWarning("PatientId is required.");
+        return null;
+    }
     var result = await _httpClient.ReadAsync("Patient", patientId, cancellationToken);

Per coding guidelines: Control flow should use guard clauses first with early return.

🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API/Services/Fhir/EpicFhirContext.cs`:
- Around line 24-31: The EpicFhirContext constructor lacks null-guard checks for
its DI parameters; add guard clauses at the start of the
EpicFhirContext(IFhirHttpClientProvider clientProvider,
ILogger<EpicFhirContext<TResource>> logger) constructor to throw
ArgumentNullException when clientProvider or logger is null (before assigning to
_clientProvider, _logger or setting _resourceType), ensuring the class fails
fast on misconfigured dependencies.
- Around line 74-85: Add a guard clause at the start of SearchAsync to validate
the query parameter: if string.IsNullOrWhiteSpace(query) then immediately fail
fast (e.g., throw an ArgumentException or return a failed
Result<IReadOnlyList<TResource>> with a clear message like "query cannot be
empty"), so you do not construct the HttpRequestMessage or call
_clientProvider/GetAuthenticatedClientAsync and do not call the FHIR endpoint
for empty queries; update SearchAsync to reference the query parameter and keep
the rest of the method (request construction using _resourceType) unchanged.
- Around line 34-42: In ReadAsync (EpicFhirContext.ReadAsync) add an early guard
clause that validates the id parameter (e.g., if string.IsNullOrWhiteSpace(id)
throw new ArgumentException("id cannot be null or empty", nameof(id))); do this
before calling _clientProvider.GetAuthenticatedClientAsync or building the
request URL (before using $"{_resourceType}/{id}") so you avoid constructing a
malformed URL and a needless network call.
- Around line 112-123: Add a null-guard at the start of CreateAsync to reject
null TResource inputs before any serialization or HTTP calls: inside CreateAsync
(before calling _clientProvider.GetAuthenticatedClientAsync and before
JsonContent.Create(resource)) check if resource is null and return a failed
Result<TResource> (using the project’s standard failure construction) with an
appropriate error message; this prevents sending null payloads and follows the
guard-clause pattern.

In `@apps/gateway/Gateway.API/Services/FhirClient.cs`:
- Around line 56-65: Add a guard clause to each public search method
(SearchConditionsAsync, SearchObservationsAsync, SearchProceduresAsync,
SearchDocumentsAsync) that checks string.IsNullOrWhiteSpace(patientId) before
composing the query; if the patientId is blank return early with an empty list
wrapped in a completed Task (e.g., Task.FromResult(new List<...>())) matching
the method's return type, otherwise continue to call SearchAndMapAsync as
before.
- Around line 106-119: Add a guard clause at the start of
GetDocumentContentAsync to validate documentId (use string.IsNullOrWhiteSpace)
and avoid calling _httpClient.ReadBinaryAsync with an empty identifier; if
invalid, log a warning with the documentId value and return null (consistent
with the existing null-return pattern and similar methods like GetPatientAsync)
so the HTTP client is never invoked with bad input.

In `@apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs`:
- Around line 33-43: Add guard clauses at the top of the JwtBackendTokenStrategy
constructor to validate incoming dependencies: check that httpClientFactory,
options, options.Value, tokenCache, and logger are not null and throw
ArgumentNullException with the appropriate parameter name if any are null;
perform these checks before assigning to fields (_httpClientFactory, _options,
_tokenCache, _logger) so the constructor fails fast with clear messages.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs`:
- Around line 114-130: In GenerateClientAssertion validate _options.ClientId
before building claims and throw an InvalidOperationException with a clear
message if it's null or empty; specifically check _options.ClientId used for
JwtRegisteredClaimNames.Iss and JwtRegisteredClaimNames.Sub and fail fast
(similar to the existing TokenEndpoint check) so you don't construct an invalid
JWT for downstream consumers.
🧹 Nitpick comments (2)
apps/gateway/Gateway.API.Tests/Middleware/CdsHookTokenMiddlewareTests.cs (2)

32-79: Tests cover the happy path well, though there's some overlap.

ExtractsTokenFromFhirAuthorization and SetsHttpContextItem test nearly identical scenarios. Consider consolidating or differentiating them more clearly (e.g., one could test a minimal payload, the other a full CDS Hook structure).


12-21: Consider adding a test for empty or whitespace-only access_token.

The middleware checks !string.IsNullOrEmpty(token) before storing, but there's no test verifying this behavior. A test with "access_token": "" or "access_token": " " would confirm tokens aren't stored when empty/whitespace.

🧪 Example test
[Test]
public async Task CdsHookTokenMiddleware_InvokeAsync_SkipsEmptyAccessToken()
{
    // Arrange
    var context = CreateHttpContext("/cds-hooks/test", "POST", """
        {
            "hook": "order-select",
            "fhirAuthorization": {
                "access_token": ""
            }
        }
        """);

    var sut = new CdsHookTokenMiddleware(CreateNextDelegate(), _logger);

    // Act
    await sut.InvokeAsync(context);

    // Assert
    await Assert.That(context.Items.ContainsKey("FhirAccessToken")).IsFalse();
    await Assert.That(_nextCalled).IsTrue();
}

rsalus added a commit that referenced this pull request Jan 31, 2026
Fixes:
- P2 Major (3 issues):
  - RSA key disposal (already fixed in prior commit)
  - Missing guard clauses in TokenStrategyResolver
  - Remove synthetic ID generation in FhirClient mappers
- P3 Minor (8 issues):
  - Guard clauses for JwtBackendTokenStrategy + ClientId validation
  - Guard clauses for CdsHookTokenMiddleware + JSON object check
  - Guard clauses for EpicFhirContext (constructor, ReadAsync, SearchAsync, CreateAsync)
  - Guard clauses for FhirHttpClient and FhirHttpClientProvider
  - Guard clauses for CdsHookTokenStrategy
  - Guard clauses for BaseFhirRepository (GetByIdAsync, FindByPatientAsync)
  - Guard clauses for FhirClient methods (GetPatientAsync, search methods, GetDocumentContentAsync)
  - Guard clauses for DependencyExtensions methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/Gateway.API/Services/FhirClient.cs (1)

22-26: ⚠️ Potential issue | 🟡 Minor

Missing guard clauses in constructor.

Per coding guidelines, constructor parameters should be validated before assignment.

🛡️ Proposed fix
 public FhirClient(IFhirHttpClient httpClient, ILogger<FhirClient> logger)
 {
+    ArgumentNullException.ThrowIfNull(httpClient);
+    ArgumentNullException.ThrowIfNull(logger);
+
     _httpClient = httpClient;
     _logger = logger;
 }
🧹 Nitpick comments (3)
apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs (1)

159-170: Consider wrapping file read in try-catch for clearer error messaging.

File.ReadAllText throws FileNotFoundException or IOException if the key file is missing or inaccessible. The current exception propagates up but the error message won't indicate which configuration option is at fault.

💡 Optional improvement for better diagnostics
 private RSA LoadPrivateKey()
 {
     if (string.IsNullOrEmpty(_options.PrivateKeyPath))
     {
         throw new InvalidOperationException("PrivateKeyPath is not configured");
     }

+    if (!File.Exists(_options.PrivateKeyPath))
+    {
+        throw new InvalidOperationException($"Private key file not found: {_options.PrivateKeyPath}");
+    }
+
     var rsa = RSA.Create();
-    var keyPem = File.ReadAllText(_options.PrivateKeyPath);
-    rsa.ImportFromPem(keyPem);
+    try
+    {
+        var keyPem = File.ReadAllText(_options.PrivateKeyPath);
+        rsa.ImportFromPem(keyPem);
+    }
+    catch (Exception ex) when (ex is IOException or CryptographicException)
+    {
+        rsa.Dispose();
+        throw new InvalidOperationException($"Failed to load private key from {_options.PrivateKeyPath}", ex);
+    }
     return rsa;
 }
apps/gateway/Gateway.API/Services/Fhir/EpicFhirContext.cs (1)

169-198: Add defensive check for entries array type.

EnumerateArray() throws InvalidOperationException if entries is not a JSON array. Malformed FHIR responses could have entry as a non-array value.

🛡️ Proposed defensive check
 private IReadOnlyList<TResource> ExtractResourcesFromBundle(JsonElement bundle)
 {
     var results = new List<TResource>();

-    if (!bundle.TryGetProperty("entry", out var entries))
+    if (!bundle.TryGetProperty("entry", out var entries) ||
+        entries.ValueKind != JsonValueKind.Array)
     {
         return results;
     }
apps/gateway/Gateway.API/Services/FhirClient.cs (1)

163-176: Add defensive check for entries array type.

Same issue as in EpicFhirContext: EnumerateArray() throws if entries is not a JSON array. Malformed FHIR responses could cause runtime exceptions.

🛡️ Proposed fix
     var json = result.Value!;
-    if (!json.TryGetProperty("entry", out var entries)) return results;
+    if (!json.TryGetProperty("entry", out var entries) ||
+        entries.ValueKind != JsonValueKind.Array)
+    {
+        return results;
+    }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/Gateway.API/Services/Fhir/FhirHttpClient.cs (1)

34-167: ⚠️ Potential issue | 🟡 Minor

Add guard clauses for string parameters before building URLs and payloads.
This aligns with the guard-clause-first guideline and follows the validation pattern established in the constructor. Apply to ReadAsync, SearchAsync, CreateAsync, and ReadBinaryAsync.

🔧 Suggested fix (example for ReadAsync)
 public async Task<Result<JsonElement>> ReadAsync(
     string resourceType,
     string id,
     CancellationToken ct = default)
 {
+    ArgumentException.ThrowIfNullOrWhiteSpace(resourceType);
+    ArgumentException.ThrowIfNullOrWhiteSpace(id);
+
     try
     {
         var httpClient = await _clientProvider.GetAuthenticatedClientAsync(ct);
🤖 Fix all issues with AI agents
In `@apps/gateway/Gateway.API/Services/Fhir/EpicFhirContext.cs`:
- Around line 111-115: The POST request is sending JSON with the default
Content-Type application/json, which violates FHIR R4; update the request
creation in EpicFhirContext (the block that gets the HttpClient via
_clientProvider.GetAuthenticatedClientAsync and calls ConfigureRequest) to set
the request.Content's Content-Type to "application/fhir+json" after assigning
JsonContent.Create(resource) (e.g., request.Content.Headers.ContentType = new
MediaTypeHeaderValue("application/fhir+json")); ensure this change is applied
where request.Content = JsonContent.Create(resource) is done and that
ConfigureRequest remains responsible only for Accept headers.

rsalus added a commit that referenced this pull request Jan 31, 2026
Fixes:
- Major: Set FHIR Content-Type header for POST requests in EpicFhirContext
- Major: Update System.IdentityModel.Tokens.Jwt to 8.15.0
- Minor: Add JsonException handling in EpicFhirContext
- Minor: Add guard clauses to FhirClient constructor
- Minor: Delete unused EpicAuthOptions class
- Minor: Fix CreateClient test verification overload

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/gateway/Gateway.API.Tests/Services/Http/JwtBackendTokenStrategyTests.cs`:
- Around line 126-154: The test assigns null to a non-nullable
EpicFhirOptions.ClientId causing a static-analysis warning; to keep the intended
null-behavior for the
JwtBackendTokenStrategy_GenerateClientAssertion_ThrowsWhenClientIdIsNull test,
update the test to use the null-forgiving operator when setting ClientId (e.g.,
assign null! to ClientId) so the compile-time warning is suppressed, then run
the test flow through CreateSut and call GenerateClientAssertion as before.
🧹 Nitpick comments (2)
apps/gateway/Gateway.API.Tests/Configuration/EpicFhirOptionsTests.cs (1)

44-70: Consider using a non-default SigningAlgorithm value to strengthen test coverage.

The test sets SigningAlgorithm to "RS384", which is already the default value. To actually verify that explicit configuration values bind correctly (rather than just confirming the default), consider using a different valid algorithm like "RS256" or "RS512".

💡 Suggested improvement
             ["Epic:FhirBaseUrl"] = "https://fhir.epic.com/api/FHIR/R4",
             ["Epic:ClientId"] = "test-client-id",
             ["Epic:PrivateKeyPath"] = "/path/to/private.key",
-            ["Epic:SigningAlgorithm"] = "RS384"
+            ["Epic:SigningAlgorithm"] = "RS512"
         })
         .Build();

And update the assertion:

-    await Assert.That(options.SigningAlgorithm).IsEqualTo("RS384");
+    await Assert.That(options.SigningAlgorithm).IsEqualTo("RS512");
apps/gateway/Gateway.API.Tests/Services/Http/JwtBackendTokenStrategyTests.cs (1)

329-443: Reduce duplication across token exchange tests.

The four AcquireTokenAsync tests (lines 329-485) share nearly identical setup: creating MockHttpMessageHandler, HttpClient, mock IHttpClientFactory, MemoryCache, EpicFhirOptions, and the SUT. Consider extracting a helper method:

private (JwtBackendTokenStrategy Sut, MemoryCache Cache, MockHttpMessageHandler Handler) 
    CreateSutWithHttpMock(string responseContent, HttpStatusCode statusCode)
{
    var mockHandler = new MockHttpMessageHandler(responseContent, statusCode);
    var httpClient = new HttpClient(mockHandler);
    
    var httpClientFactory = Substitute.For<IHttpClientFactory>();
    httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
    
    var cache = new MemoryCache(new MemoryCacheOptions());
    var options = new EpicFhirOptions
    {
        FhirBaseUrl = "https://fhir.epic.com/api/FHIR/R4",
        ClientId = "test-client-id",
        TokenEndpoint = "https://fhir.epic.com/oauth2/token",
        PrivateKeyPath = _testKeyPath,
        SigningAlgorithm = "RS384"
    };
    
    var sut = new JwtBackendTokenStrategy(
        httpClientFactory,
        Options.Create(options),
        cache,
        _logger);
        
    return (sut, cache, mockHandler);
}

This would reduce each test to ~10 lines and improve maintainability.

@rsalus
Copy link
Contributor Author

rsalus commented Jan 31, 2026

@coderabbitai If there are no remaining unaddressed critical/major issues, approve this PR.

@coderabbitai
Copy link

coderabbitai bot commented Jan 31, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ArgumentNullException.ThrowIfNull usage in the service files
echo "=== Checking for ArgumentNullException.ThrowIfNull in service files ==="
rg -n "ArgumentNullException\.ThrowIfNull" apps/gateway/Gateway.API/Services/

echo -e "\n=== Checking guard clauses in key service constructors and methods ==="
# Check FhirClient.cs for null checks
echo "--- FhirClient.cs ---"
rg -n -A2 -B2 "public.*FhirClient|public.*Task" apps/gateway/Gateway.API/Services/FhirClient.cs | head -50

echo -e "\n--- DocumentUploader.cs ---"
rg -n -A2 -B2 "public.*DocumentUploader|public.*Task" apps/gateway/Gateway.API/Services/DocumentUploader.cs | head -30

echo -e "\n--- FhirDataAggregator.cs ---"
rg -n -A2 -B2 "public.*FhirDataAggregator|public.*Task" apps/gateway/Gateway.API/Services/FhirDataAggregator.cs | head -30

echo -e "\n=== Checking DependencyExtensions.cs for null checks ==="
rg -n "ArgumentNullException" apps/gateway/Gateway.API/DependencyExtensions.cs

Length of output: 5850


🏁 Script executed:

#!/bin/bash
# Check the new HTTP service classes for guard clauses
echo "=== Checking HTTP service classes for null checks ==="

echo "--- CdsHookTokenStrategy.cs ---"
cat apps/gateway/Gateway.API/Services/Http/CdsHookTokenStrategy.cs

echo -e "\n--- JwtBackendTokenStrategy.cs constructor ---"
rg -n -A10 "public.*JwtBackendTokenStrategy\(" apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs

echo -e "\n--- FhirHttpClientProvider.cs constructor ---"
rg -n -A10 "public.*FhirHttpClientProvider\(" apps/gateway/Gateway.API/Services/Http/FhirHttpClientProvider.cs

echo -e "\n--- TokenStrategyResolver.cs ---"
cat apps/gateway/Gateway.API/Services/Http/TokenStrategyResolver.cs

Length of output: 3601


🏁 Script executed:

#!/bin/bash
# Check BaseFhirRepository for guard clauses
echo "=== Checking BaseFhirRepository.cs ==="
cat apps/gateway/Gateway.API/Services/Fhir/BaseFhirRepository.cs | head -80

Length of output: 2686


🏁 Script executed:

#!/bin/bash
# Check for potential resource leaks or disposal issues
echo "=== Checking for IDisposable usage and disposal patterns ==="
rg -n "IDisposable|using\s+\(" apps/gateway/Gateway.API/Services/Http/ apps/gateway/Gateway.API/Middleware/

echo -e "\n=== Checking JWT security - private key handling ==="
rg -n -A5 -B5 "LoadPrivateKey|RSA\.Create" apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs

echo -e "\n=== Checking for hardcoded secrets or tokens ==="
rg -n -i "password|secret|key.*=.*['\"]" apps/gateway/Gateway.API/Services/Http/ apps/gateway/Gateway.API/Configuration/

echo -e "\n=== Checking middleware registration ==="
rg -n "CdsHookTokenMiddleware|UseMiddleware" apps/gateway/Gateway.API/

Length of output: 2343


🏁 Script executed:

#!/bin/bash
# Check for proper token caching and expiration
echo "=== Checking token cache expiration settings ==="
rg -n -A3 -B3 "AbsoluteExpirationRelativeToNow|SlidingExpiration" apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs

echo -e "\n=== Checking DocumentUploader and FhirDataAggregator for null checks ==="
cat apps/gateway/Gateway.API/Services/DocumentUploader.cs | head -40

echo -e "\n--- FhirDataAggregator constructor ---"
cat apps/gateway/Gateway.API/Services/FhirDataAggregator.cs | head -35

Length of output: 2756


🏁 Script executed:

#!/bin/bash
# Check the full token cache implementation
echo "=== Checking token cache implementation and expiration ==="
rg -n -A10 "SetAsync|Set\(" apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs

echo -e "\n=== Checking if middleware is registered in Program.cs or Startup ==="
fd -e cs "Program.cs|Startup.cs" apps/gateway/Gateway.API/ --exec cat {}

echo -e "\n=== Verify DocumentUploader has no guard clauses ==="
cat apps/gateway/Gateway.API/Services/DocumentUploader.cs | head -35

Length of output: 3694


@rsalus I've completed a thorough review and found two critical/major issues that need to be addressed before approval:

🔴 Critical Issue: Missing Middleware Registration

The CdsHookTokenMiddleware was created to extract fhirAuthorization.access_token from CDS Hook POST payloads, but it's not registered in Program.cs. Without registration, the dual authentication strategy won't work for CDS Hook requests.

Required fix in Program.cs:

app.UseCors();
app.UseMiddleware<CdsHookTokenMiddleware>(); // Add this line
app.UseHealthChecks("/health");

🟠 Major Issue: Incomplete Guard Clause Coverage

While new HTTP services have proper ArgumentNullException.ThrowIfNull guard clauses, two existing services modified in this PR are still missing them:

  1. DocumentUploader constructor (lines 24-33):
public DocumentUploader(
    IFhirHttpClient fhirHttpClient,
    ILogger<DocumentUploader> logger,
    IOptions<DocumentOptions> options)
{
    ArgumentNullException.ThrowIfNull(fhirHttpClient);
    ArgumentNullException.ThrowIfNull(logger);
    ArgumentNullException.ThrowIfNull(options);
    
    _fhirHttpClient = fhirHttpClient;
    _logger = logger;
    _options = options.Value;
}
  1. FhirDataAggregator constructor (lines 24-31):
public FhirDataAggregator(
    IFhirClient fhirClient,
    IOptions<ClinicalQueryOptions> options,
    ILogger<FhirDataAggregator> logger)
{
    ArgumentNullException.ThrowIfNull(fhirClient);
    ArgumentNullException.ThrowIfNull(options);
    ArgumentNullException.ThrowIfNull(logger);
    
    _fhirClient = fhirClient;
    _options = options.Value;
    _logger = logger;
}

✅ What's Working Well

  • All new HTTP service classes have proper guard clauses
  • Token caching properly configured with 55-minute expiration
  • RSA key disposal handled correctly with using var
  • Comprehensive test coverage (46 new tests)
  • No hardcoded secrets detected

I cannot approve until these issues are resolved, as they directly address your original concern about guard clauses and would cause the CDS Hook authentication flow to fail.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/Gateway.API/Services/DocumentUploader.cs (1)

39-52: ⚠️ Potential issue | 🟡 Minor

Add method-level guard clauses for inputs.

pdfBytes and patientId are used immediately; null/empty values can cause runtime errors or invalid FHIR references. Place guards at the top of the method.

✅ Proposed fix
 public async Task<Result<string>> UploadDocumentAsync(
     byte[] pdfBytes,
     string patientId,
     string? encounterId,
     CancellationToken cancellationToken = default)
 {
+    ArgumentNullException.ThrowIfNull(pdfBytes);
+    if (string.IsNullOrWhiteSpace(patientId))
+    {
+        throw new ArgumentException("Patient ID is required.", nameof(patientId));
+    }
+
     _logger.LogInformation(
         "Uploading PA form. Size={Size} bytes",
         pdfBytes.Length);

As per coding guidelines: Guard clauses first, early return, no arrow code.

🧹 Nitpick comments (2)
apps/gateway/Gateway.API/Services/FhirDataAggregator.cs (1)

39-55: Add guard clause for patientId parameter.

The method logs and makes multiple FHIR API calls using patientId without validating it first. While the downstream IFhirClient methods now have their own guards, defense-in-depth and consistent error reporting at the aggregation layer would be beneficial.

🛡️ Proposed fix
 public async Task<ClinicalBundle> AggregateClinicalDataAsync(
     string patientId,
     CancellationToken cancellationToken = default)
 {
+    ArgumentException.ThrowIfNullOrWhiteSpace(patientId);
+
     _logger.LogInformation("Aggregating clinical data for patient {PatientId}", patientId);

As per coding guidelines: "Guard clauses first, early return, no arrow code"

apps/gateway/Gateway.API.Tests/Services/Http/JwtBackendTokenStrategyTests.cs (1)

67-94: Use TUnit's fluent exception assertion pattern to simplify repetitive try-catch blocks.

The manual try-catch is verbose and repeats identically across four tests. TUnit's Assert.That(() => …).Throws<T>() pattern is cleaner and more idiomatic.

♻️ Cleaner exception assertion pattern
await Assert.That(() => sut.GenerateClientAssertion())
    .Throws<InvalidOperationException>()
    .WithMessage("TokenEndpoint is not configured for JWT generation");

Consider consolidating the four exception tests (TokenEndpointIsNull, TokenEndpointIsEmpty, ClientIdIsNull, ClientIdIsEmpty) into a single parameterized test to further reduce duplication.

@rsalus
Copy link
Contributor Author

rsalus commented Jan 31, 2026

@coderabbitai If there are no remaining unaddressed critical/major issues, approve this PR.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 31, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 31, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== 1. Verify CdsHookTokenMiddleware registration in Program.cs ==="
rg -n "UseMiddleware.*CdsHookTokenMiddleware" apps/gateway/Gateway.API/Program.cs

echo -e "\n=== 2. Verify DocumentUploader constructor has guard clauses ==="
rg -n -A15 "public DocumentUploader\(" apps/gateway/Gateway.API/Services/DocumentUploader.cs

echo -e "\n=== 3. Verify FhirDataAggregator constructor has guard clauses ==="
rg -n -A15 "public FhirDataAggregator\(" apps/gateway/Gateway.API/Services/FhirDataAggregator.cs

echo -e "\n=== 4. Check for any TODO or FIXME comments that might indicate incomplete work ==="
rg -n "TODO|FIXME" apps/gateway/Gateway.API/Services/Http/ apps/gateway/Gateway.API/Middleware/

echo -e "\n=== 5. Verify all new HTTP services have proper guard clauses ==="
echo "--- CdsHookTokenStrategy ---"
rg -n -A5 "public CdsHookTokenStrategy\(" apps/gateway/Gateway.API/Services/Http/CdsHookTokenStrategy.cs

echo -e "\n--- JwtBackendTokenStrategy ---"
rg -n -A10 "public JwtBackendTokenStrategy\(" apps/gateway/Gateway.API/Services/Http/JwtBackendTokenStrategy.cs

echo -e "\n--- FhirHttpClientProvider ---"
rg -n -A8 "public FhirHttpClientProvider\(" apps/gateway/Gateway.API/Services/Http/FhirHttpClientProvider.cs

echo -e "\n--- TokenStrategyResolver ---"
rg -n -A8 "public TokenStrategyResolver\(" apps/gateway/Gateway.API/Services/Http/TokenStrategyResolver.cs

Length of output: 3058


@rsalus Excellent work! All previously identified critical and major issues have been successfully resolved:

✅ Issues Resolved

1. Critical: Middleware Registration
CdsHookTokenMiddleware is now properly registered in Program.cs at line 61, ensuring CDS Hook authentication will function correctly.

2. Major: Guard Clauses
Both DocumentUploader and FhirDataAggregator constructors now include complete ArgumentNullException.ThrowIfNull checks for all dependencies.

🎯 PR Quality Summary

  • Guard clauses: Complete coverage across all service constructors
  • Test coverage: 46 new comprehensive tests
  • Security: Proper RSA key disposal, no hardcoded secrets
  • Token caching: Correctly configured (55-minute expiration)
  • Architecture: Clean dual-auth strategy pattern implementation

No remaining critical or major issues found. The PR implements a robust Epic FHIR integration with proper error handling, security practices, and test coverage.

✅ Actions performed

Comments resolved and changes approved.

- Add JwtBackendTokenStrategy for backend JWT authentication
- Add CdsHookTokenStrategy for extracting tokens from CDS Hook requests
- Add FhirHttpClientProvider for authenticated FHIR API access
- Add CdsHookTokenMiddleware for request token extraction
- Add ITokenStrategyResolver interface for strategy selection
- Add AddEpicFhirServices DI registration method
- Add comprehensive tests for all new components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@rsalus rsalus force-pushed the feature/epic-integration branch from 27e0c49 to ce27023 Compare January 31, 2026 09:05
@rsalus rsalus merged commit 81f8042 into main Jan 31, 2026
4 of 6 checks passed
@github-project-automation github-project-automation bot moved this from Todo to Done in Authscript Demo Jan 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant