Skip to content

Fix MCEDT/HCV keystore race condition causing upload failures#2228

Merged
yingbull merged 6 commits intohotfix/01232026from
fix/mcedt-hcv-keystore-race-condition
Jan 30, 2026
Merged

Fix MCEDT/HCV keystore race condition causing upload failures#2228
yingbull merged 6 commits intohotfix/01232026from
fix/mcedt-hcv-keystore-race-condition

Conversation

@D3V41
Copy link
Copy Markdown
Collaborator

@D3V41 D3V41 commented Jan 29, 2026

Summary

  • Fixes race condition where MCEDT document uploads fail after HCV health card validation
  • Changed clientKeystore from static to instance variable in EdtClientBuilder
  • Each service (MCEDT, HCV) now maintains independent keystore configuration

Problem

The clientKeystore variable in EdtClientBuilder was declared as static, meaning it was shared across all instances. This caused a critical race condition:

  1. MCEDT upload sets static var → /var/lib/mcedt/clientKeystore.properties
  2. HCV validation overwrites static var → /var/lib/hcv/clientKeystore.properties ⚠️
  3. MCEDT upload (again) tries to use → HCV keystore ❌ Authentication fails

Solution

Made clientKeystore an instance variable instead of static:

Files Modified

EdtClientBuilder.java (src/main/java/ca/openosp/openo/integration/ebs/client/ng/)

// Before (static - shared across all instances)                                                                                                                                                                                                   
private static String clientKeystore = DEFAULT_CLIENT_KEYSTORE;                                                                                                                                                                                    
public static void setClientKeystoreFilename(String filename) { ... }                                                                                                                                                                              
                                                                                                                                                                                                                                                   
// After (instance - each builder has its own)                                                                                                                                                                                                     
private String clientKeystore = DEFAULT_CLIENT_KEYSTORE;                                                                                                                                                                                           
public void setClientKeystoreFilename(String filename) { ... }                                                                                                                                                                                     
                                                                                                                                                                                                                                                   
DelegateFactory.java (src/main/java/ca/openosp/openo/integration/mcedt/)                                                                                                                                                                           
- Updated setExternalClientKeystoreFilename() to accept EdtClientBuilder instance                                                                                                                                                                  
- Now calls instance method: builder.setClientKeystoreFilename()                                                                                                                                                                                   
                                                                                                                                                                                                                                                   
OnlineHCValidator.java (src/main/java/ca/openosp/openo/integration/mchcv/)                                                                                                                                                                         
- Updated setExternalClientKeystoreFilename() to accept EdtClientBuilder instance                                                                                                                                                                  
- Now calls instance method: builder.setClientKeystoreFilename()                                                                                                                                                                                   
                                                                                                                                                                                                                                                   
EDTBaseTest.java (src/test/java/ca/openosp/openo/integration/mcedt/)                                                                                                                                                                               
- Updated test helper method signature to match new pattern

## Summary by Sourcery

Isolate EDT client keystore configuration per service to resolve race conditions between concurrent MCEDT uploads and HCV validations.

Bug Fixes:
- Prevent MCEDT uploads from failing due to HCV validation overwriting a shared static EDT client keystore configuration.

Enhancements:
- Make EdtClientBuilder use an instance-level client keystore path and pass builder instances into MCEDT and HCV configuration helpers so each service can configure its own keystore independently.
- Improve JavaDoc for keystore configuration helpers and EdtClientBuilder to clarify per-service behavior and defaults.

Tests:
- Update MCEDT EDTBaseTest helper to configure client keystore on the provided EdtClientBuilder instance, aligning tests with the new non-static keystore configuration.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes a race condition that caused MCEDT uploads to fail after HCV validation by making the EDT client keystore instance-based. Each service now keeps its own keystore path, preventing cross-service overwrites and authentication failures.

- **Bug Fixes**
- Made EdtClientBuilder.clientKeystore an instance variable and added an instance setter.
- Updated MCEDT DelegateFactory and HCV OnlineHCValidator to configure keystore on the provided builder; treat empty/blank paths as null to use the default.
- Added tests for per-instance isolation and fallback behavior; updated EDTBaseTest.

<sup>Written for commit 73efa77a54d5b1070e64a905803d850fd20179ca. Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Refactor**
* Updated internal keystore configuration management to use per-instance handling instead of shared global state, enhancing isolation and flexibility across multiple concurrent builder instances.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

D3V41 and others added 2 commits January 29, 2026 12:23
…re instance-based

The static `clientKeystore` variable in EdtClientBuilder was shared between  MCEDT and HCV services, causing a race condition where HCV validation would overwrite the MCEDT keystore path. This resulted in MCEDT upload failures  after HCV validation operations.
Enhance JavaDoc documentation for setClientKeystoreFilename and
setExternalClientKeystoreFilename methods to comply with CLAUDE.md
standards. Added detailed descriptions, @param tags with specific
data types, and @SInCE tags based on git history.

- EdtClientBuilder.setClientKeystoreFilename: Added context about
  instance-based keystore paths preventing race conditions
- DelegateFactory/OnlineHCValidator/EDTBaseTest: Converted C-style
  comments to proper JavaDoc with comprehensive parameter documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Jan 29, 2026

Reviewer's Guide

Converts the EDT client keystore configuration from a static, shared setting to an instance-level setting on EdtClientBuilder, and updates MCEDT and HCV callers (plus tests) to pass and configure the builder explicitly so each service uses its own keystore and avoids race conditions.

Sequence diagram for independent MCEDT and HCV keystore configuration

sequenceDiagram
    actor User
    participant MCEDTService as MCEDTService
    participant HCVService as HCVService
    participant DelegateFactory as DelegateFactory
    participant OnlineHCValidator as OnlineHCValidator
    participant EdtClientBuilder_MCEDT as EdtClientBuilder_MCEDT
    participant EdtClientBuilder_HCV as EdtClientBuilder_HCV

    User->>MCEDTService: triggerUpload()
    MCEDTService->>DelegateFactory: newDelegate(serviceId)
    DelegateFactory->>EdtClientBuilder_MCEDT: new EdtClientBuilder(config)
    DelegateFactory->>DelegateFactory: setExternalClientKeystoreFilename(EdtClientBuilder_MCEDT, mcedtKeystorePath)
    DelegateFactory->>EdtClientBuilder_MCEDT: setClientKeystoreFilename(mcedtKeystoreUrl)
    DelegateFactory->>EdtClientBuilder_MCEDT: build(EDTDelegate)

    User->>HCVService: validateHealthCard()
    HCVService->>OnlineHCValidator: new OnlineHCValidator()
    OnlineHCValidator->>EdtClientBuilder_HCV: new EdtClientBuilder(config)
    OnlineHCValidator->>OnlineHCValidator: setExternalClientKeystoreFilename(EdtClientBuilder_HCV, hcvKeystorePath)
    OnlineHCValidator->>EdtClientBuilder_HCV: setClientKeystoreFilename(hcvKeystoreUrl)
    OnlineHCValidator->>EdtClientBuilder_HCV: build(HCValidation)

    Note over EdtClientBuilder_MCEDT,EdtClientBuilder_HCV: Each EdtClientBuilder instance now maintains its own clientKeystore value, eliminating cross-service race conditions.
Loading

Updated class diagram for EdtClientBuilder and callers

classDiagram
    class EdtClientBuilder {
        - static AtomicBoolean isInitialized
        - static String DEFAULT_CLIENT_KEYSTORE
        - String clientKeystore
        - EdtClientBuilderConfig config
        + EdtClientBuilder(EdtClientBuilderConfig config)
        + void setClientKeystoreFilename(String filename)
        + <T> T build(Class serviceClass)
        + static void configureSsl(HTTPConduit conduit)
    }

    class DelegateFactory {
        - static Properties props
        - static UserPropertyDAO userPropertyDAO
        + static EDTDelegate newDelegate(String serviceId)
        + static void setUserPropertyDAO(UserPropertyDAO userPropertyDAO)
        - static void setExternalClientKeystoreFilename(EdtClientBuilder builder, String clientKeystorePropertiesPath)
    }

    class OnlineHCValidator {
        - static Logger logger
        - EdtClientBuilder builder
        - HCValidation validation
        + OnlineHCValidator()
        - void setBuilder(EdtClientBuilder builder)
        - static void setExternalClientKeystoreFilename(EdtClientBuilder builder, String clientKeystorePropertiesPath)
    }

    class EDTDelegate
    class HCValidation
    class EdtClientBuilderConfig
    class HTTPConduit
    class UserPropertyDAO

    DelegateFactory --> EdtClientBuilder : creates
    DelegateFactory --> EDTDelegate : builds
    OnlineHCValidator --> EdtClientBuilder : creates
    OnlineHCValidator --> HCValidation : builds
    EdtClientBuilder --> EdtClientBuilderConfig : uses
    EdtClientBuilder ..> HTTPConduit : configureSsl()
    DelegateFactory ..> UserPropertyDAO : uses
Loading

File-Level Changes

Change Details Files
Make EdtClientBuilder keystore configuration instance-specific instead of static and document the behavior.
  • Change clientKeystore field from static to an instance field initialized to the default keystore.
  • Change setClientKeystoreFilename from static to an instance method that sets this builder’s keystore path.
  • Expand Javadoc on setClientKeystoreFilename to describe per-instance behavior, usage, and intent to avoid race conditions.
src/main/java/ca/openosp/openo/integration/ebs/client/ng/EdtClientBuilder.java
Update MCEDT delegate factory to configure an EdtClientBuilder instance with its own keystore path.
  • Change setExternalClientKeystoreFilename to accept an EdtClientBuilder parameter and call builder.setClientKeystoreFilename instead of the static method.
  • Update newDelegate to pass the created EdtClientBuilder into setExternalClientKeystoreFilename.
  • Improve Javadoc on setExternalClientKeystoreFilename to clarify MCEDT-specific behavior and defaulting rules.
src/main/java/ca/openosp/openo/integration/mcedt/DelegateFactory.java
Update HCV OnlineHCValidator to configure its own EdtClientBuilder instance with a separate keystore path.
  • Construct an EdtClientBuilder into a local variable (hcvBuilder), pass it into setBuilder, and into setExternalClientKeystoreFilename.
  • Change setExternalClientKeystoreFilename to accept an EdtClientBuilder parameter and delegate to builder.setClientKeystoreFilename instead of the static method.
  • Replace block comment with Javadoc describing HCV-specific keystore configuration behavior and fallback to default keystore.
src/main/java/ca/openosp/openo/integration/mchcv/OnlineHCValidator.java
Align MCEDT test helper with the new instance-based keystore configuration API.
  • Change test helper setExternalClientKeystoreFilename to accept an EdtClientBuilder parameter and use builder.setClientKeystoreFilename.
  • Update EDTBaseTest.newDelegate to pass the created EdtClientBuilder into setExternalClientKeystoreFilename.
  • Add Javadoc explaining the test-specific keystore configuration behavior and defaulting rules.
src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java

Possibly linked issues

  • #[Bug]: MCEDT upload fails after HCV validation due to shared static clientKeystore variable: PR makes clientKeystore instance-based and updates callers, resolving the MCEDT/HCV keystore race described in issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@D3V41 D3V41 requested a review from Copilot January 29, 2026 17:42
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

The changes refactor keystore configuration in EdtClientBuilder from static to instance-scoped members. The clientKeystore field and setClientKeystoreFilename method are converted from class-level to instance-level, with corresponding updates to call sites across DelegateFactory, OnlineHCValidator, and EDTBaseTest to pass builder instances when configuring keystores.

Changes

Cohort / File(s) Summary
EdtClientBuilder
src/main/java/ca/openosp/openo/integration/ebs/client/ng/EdtClientBuilder.java
Converted clientKeystore field from static to instance member and changed setClientKeystoreFilename method from static to instance scope. Updated Javadoc to reflect per-instance keystore management.
Keystore Configuration Call Sites
src/main/java/ca/openosp/openo/integration/mcedt/DelegateFactory.java, src/main/java/ca/openosp/openo/integration/mchcv/OnlineHCValidator.java, src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java
Updated setExternalClientKeystoreFilename helper method signature to accept EdtClientBuilder instance as first parameter. Modified all call sites to pass the builder instance and updated the helper to configure keystores via the provided builder instance instead of static calls. Added Javadoc documentation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 From static fields to instances so bright,
A builder now holds its own keystore tight,
No shared state causing concurrent plight,
Each instance stands alone, configured just right! 🔐

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 accurately reflects the main change: converting a static keystore field to instance-based to fix a race condition between MCEDT and HCV services.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@D3V41
Copy link
Copy Markdown
Collaborator Author

D3V41 commented Jan 29, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • The three setExternalClientKeystoreFilename helpers in DelegateFactory, OnlineHCValidator, and EDTBaseTest now share almost identical logic; consider extracting a common utility or adding a EdtClientBuilder.setClientKeystoreFromPath(String) method to avoid duplication and keep the behavior consistent in one place.
  • The setExternalClientKeystoreFilename methods are still static but now mutate the passed-in EdtClientBuilder instance; it may be clearer and less error-prone to make these instance-level helpers (or move the logic onto EdtClientBuilder) to better reflect the instance-scoped keystore configuration.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The three `setExternalClientKeystoreFilename` helpers in `DelegateFactory`, `OnlineHCValidator`, and `EDTBaseTest` now share almost identical logic; consider extracting a common utility or adding a `EdtClientBuilder.setClientKeystoreFromPath(String)` method to avoid duplication and keep the behavior consistent in one place.
- The `setExternalClientKeystoreFilename` methods are still `static` but now mutate the passed-in `EdtClientBuilder` instance; it may be clearer and less error-prone to make these instance-level helpers (or move the logic onto `EdtClientBuilder`) to better reflect the instance-scoped keystore configuration.

## Individual Comments

### Comment 1
<location> `src/main/java/ca/openosp/openo/integration/mchcv/OnlineHCValidator.java:139-142` </location>
<code_context>
+     * @since 2025-08-26
      */
-    private static void setExternalClientKeystoreFilename(String clientKeystorePropertiesPath) {
+    private static void setExternalClientKeystoreFilename(EdtClientBuilder builder, String clientKeystorePropertiesPath) {
         if (clientKeystorePropertiesPath == null) {
             return;
</code_context>

<issue_to_address>
**issue (bug_risk):** Consider treating empty/blank paths like null to avoid accidentally using the current directory as a keystore path.

Because only `null` is treated as "no override", an empty or whitespace value will still call `Files.exists` with `""`, which resolves to the current directory and almost always exists. That directory would then be used as the keystore location, likely failing in a confusing way. Please treat empty/blank values like `null` and return before calling `Files.exists`.
</issue_to_address>

### Comment 2
<location> `src/main/java/ca/openosp/openo/integration/mcedt/DelegateFactory.java:118-121` </location>
<code_context>
+     * @since 2025-08-26
      */
-    private static void setExternalClientKeystoreFilename(String clientKeystorePropertiesPath) {
+    private static void setExternalClientKeystoreFilename(EdtClientBuilder builder, String clientKeystorePropertiesPath) {
         if (clientKeystorePropertiesPath == null) {
             return;
</code_context>

<issue_to_address>
**issue:** Align handling of empty/blank keystore paths with the documented "null or does not exist" behavior.

If this property is set to an empty or whitespace-only string, `Files.exists` will likely succeed on `.` and we’ll end up using a directory URL as the keystore path, contrary to the Javadoc’s "null or does not exist" contract. Adding a `trim().isEmpty()` check alongside the null check would align behavior with the docs and prevent this misconfiguration.
</issue_to_address>

### Comment 3
<location> `src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java:340` </location>
<code_context>
         config.setMtomEnabled(true);
         EdtClientBuilder builder = new EdtClientBuilder(config);
-        setExternalClientKeystoreFilename(props.getProperty("mcedt.service.clientKeystore.properties"));
+        setExternalClientKeystoreFilename(builder, props.getProperty("mcedt.service.clientKeystore.properties"));
         EDTDelegate edtDelegate = builder.build(EDTDelegate.class);
         if (logger.isInfoEnabled()) {
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test that proves keystore configuration is isolated per EdtClientBuilder instance (regression test for the race condition).

Right now the test only updates the helper signature and doesn’t validate the new per‑builder behavior. Please add a regression test (here or in a test class near `EdtClientBuilder`) that, for example:

- Instantiates two `EdtClientBuilder`s with different keystore property paths.
- Calls `setExternalClientKeystoreFilename` on each builder separately.
- Verifies (via the built delegates or exposed configuration) that changing the keystore on one builder does not affect the other.

This ensures the test would fail if `clientKeystore` were ever made static again.

Suggested implementation:

```java
        EDTDelegate edtDelegate = builder.build(EDTDelegate.class);
        if (logger.isInfoEnabled()) {
            logger.info("Created new EDT delegate " + edtDelegate);
        return edtDelegate;
    }

    @Test
    public void clientKeystoreConfigurationIsIsolatedPerBuilderInstance() throws Exception {
        // Arrange: create two independent EDT configurations and builders
        EDTConfiguration config1 = new EDTConfiguration();
        config1.setMtomEnabled(true);
        EDTConfiguration config2 = new EDTConfiguration();
        config2.setMtomEnabled(true);

        EdtClientBuilder builder1 = new EdtClientBuilder(config1);
        EdtClientBuilder builder2 = new EdtClientBuilder(config2);

        // Act: configure different external client keystore files on each builder
        setExternalClientKeystoreFilename(builder1, "test-keystore-1.properties");
        setExternalClientKeystoreFilename(builder2, "test-keystore-2.properties");

        // Assert: client keystore configuration is not shared between builders
        java.lang.reflect.Field clientKeystoreField = EdtClientBuilder.class.getDeclaredField("clientKeystore");
        clientKeystoreField.setAccessible(true);

        Object clientKeystore1 = clientKeystoreField.get(builder1);
        Object clientKeystore2 = clientKeystoreField.get(builder2);

        org.junit.Assert.assertNotNull("First builder should have client keystore configured", clientKeystore1);
        org.junit.Assert.assertNotNull("Second builder should have client keystore configured", clientKeystore2);
        org.junit.Assert.assertNotSame("Client keystore must be isolated per builder instance", clientKeystore1, clientKeystore2);
    }

    /**
     * Set an external client keystore properties file for the EDT client builder in test context.
     * This method configures a custom keystore properties file path for MCEDT service

```

Assumptions and potential adjustments:
1. This test assumes `EDTConfiguration` has a no-arg constructor and a `setMtomEnabled(boolean)` method, and that `EdtClientBuilder` has a constructor accepting `EDTConfiguration` — both are implied by the existing code snippet. If additional configuration is required to build a valid `EdtClientBuilder` in your environment (e.g., `setServiceId` or other mandatory fields), set those on `config1` and `config2` in the test as well.
2. The test uses `@Test` and fully qualified `org.junit.Assert` to avoid modifying imports. Ensure the class already uses JUnit (which it appears to) and that the appropriate JUnit dependency is present.
3. The test relies on the existence of a non-static field named `clientKeystore` in `EdtClientBuilder`. If the field name differs, update `"clientKeystore"` accordingly. The test will fail if that field is made `static`, providing the regression protection you requested.
</issue_to_address>

### Comment 4
<location> `src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java:359-362` </location>
<code_context>
+     * @since 2025-08-26
      */
-    protected static void setExternalClientKeystoreFilename(String clientKeystorePropertiesPath) {
+    protected static void setExternalClientKeystoreFilename(EdtClientBuilder builder, String clientKeystorePropertiesPath) {
         if (clientKeystorePropertiesPath == null) {
             return;
</code_context>

<issue_to_address>
**suggestion (testing):** Extend tests to cover null and non-existent keystore path handling in the test helper.

The helper’s Javadoc promises that a null or non-existent keystore path falls back to the default keystore. Currently it returns early on null and only proceeds when `Files.exists(...)` is true, but there are no tests covering these cases. Please add tests that:

- Call the helper with `clientKeystorePropertiesPath == null` and verify it does not fail and still uses the default keystore (or leaves the configuration unchanged).
- Call it with a path to a non-existent file and verify the default keystore is still used.

This will tie the behavior to the documented contract and guard against regressions.

Suggested implementation:

```java
     * This method configures a custom keystore properties file path for MCEDT service
     * client certificate authentication during testing. If the provided path is null or the
     * file does not exist, the default keystore at src/main/resources/clientKeystore.properties
     * will be used.
     *
     * @param builder EdtClientBuilder the EDT client builder instance to configure
     * @param clientKeystorePropertiesPath String the absolute path to the client keystore properties file, or null to use default
     * @since 2025-08-26
     */
    protected static void setExternalClientKeystoreFilename(EdtClientBuilder builder, String clientKeystorePropertiesPath) {
        if (clientKeystorePropertiesPath == null) {
            return;
        }
        if (Files.exists(signaturePropFile)) {
            File file = new File(clientKeystorePropertiesPath);
            try {
                builder.setClientKeystoreFilename(file.toURI().toURL().toString());
            } catch (MalformedURLException e) {
                logger.error("Malformed URL: " + clientKeystorePropertiesPath, e);
            }
        }
    }

    /**
     * Tests for {@link #setExternalClientKeystoreFilename(EdtClientBuilder, String)} to verify
     * that null and non-existent keystore paths leave the builder configuration unchanged and
     * thus fall back to the default keystore, as documented in the method contract.
     */
    @Test
    void setExternalClientKeystoreFilename_nullPath_usesDefaultKeystore() {
        // given
        EdtClientBuilder builder = Mockito.mock(EdtClientBuilder.class);

        // when
        setExternalClientKeystoreFilename(builder, null);

        // then
        Mockito.verify(builder, Mockito.never()).setClientKeystoreFilename(Mockito.anyString());
    }

    @Test
    void setExternalClientKeystoreFilename_nonExistentPath_usesDefaultKeystore() {
        // given
        EdtClientBuilder builder = Mockito.mock(EdtClientBuilder.class);

        // use a path that does not exist
        Path nonExistentPath = Paths.get("target", "does-not-exist", "clientKeystore.properties");
        assertFalse(Files.exists(nonExistentPath), "Test precondition failed: non-existent path unexpectedly exists");

        // when
        setExternalClientKeystoreFilename(builder, nonExistentPath.toAbsolutePath().toString());

        // then
        Mockito.verify(builder, Mockito.never()).setClientKeystoreFilename(Mockito.anyString());
    }

```

To make the above test methods compile and integrate cleanly, you will also need to:

1. **Ensure the tests are inside the EDTBaseTest class body**
   - The replacement block above assumes that:
     - The `setExternalClientKeystoreFilename` method is inside `EDTBaseTest`.
     - The two new `@Test` methods are also members of the same class.
   - If your actual method or class structure differs (e.g. the method is in a nested helper class), move the test methods so that they remain in a JUnit test class that can access `setExternalClientKeystoreFilename`.

2. **Add the necessary imports at the top of `EDTBaseTest.java` (or adjust if they already exist):**
   ```java
   import org.junit.jupiter.api.Test;
   import static org.junit.jupiter.api.Assertions.assertFalse;

   import org.mockito.Mockito;

   import java.nio.file.Files;
   import java.nio.file.Path;
   import java.nio.file.Paths;
   ```
   - If your project uses JUnit 4 instead of JUnit 5:
     - Replace `org.junit.jupiter.api.Test` with `org.junit.Test`.
     - Replace `import static org.junit.jupiter.api.Assertions.assertFalse;` with `import static org.junit.Assert.assertFalse;`.
   - If Mockito is already statically imported (e.g. `import static org.mockito.Mockito.*;`), you can simplify the calls accordingly.

3. **Keep consistency with your existing test style**
   - If `EDTBaseTest` uses a different mocking framework or manually constructed `EdtClientBuilder` test doubles instead of Mockito, replace the `Mockito.mock(...)` and verification calls with the equivalent in your codebase.
   - If you prefer to verify the builder state via a getter instead of interaction-based testing, you can replace:
     ```java
     Mockito.verify(builder, Mockito.never()).setClientKeystoreFilename(Mockito.anyString());
     ```
     with assertions on the builder’s internal state (e.g. checking that the client keystore filename is still the default or `null`).

4. **If `signaturePropFile` is not actually derived from the passed path**
   - The non-existent path test assumes the `Files.exists(...)` check in `setExternalClientKeystoreFilename` ultimately uses the passed in `clientKeystorePropertiesPath` (directly or via `signaturePropFile`).
   - If `signaturePropFile` is currently a constant or derived differently, you may need to:
     - Refactor the method so that `signaturePropFile` is constructed from `clientKeystorePropertiesPath` (e.g. `Paths.get(clientKeystorePropertiesPath)`), or
     - Adapt the test to use whatever path the method actually checks for existence.
   - Align the implementation so that the behavior tested (null and non-existent paths falling back to the default keystore) matches the method’s Javadoc exactly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java
Comment thread src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 4 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical race condition in EDT client keystore configuration that was causing MCEDT document upload failures after HCV health card validation. The issue stemmed from clientKeystore being a static variable shared across all EdtClientBuilder instances, allowing one service (HCV) to overwrite the keystore configuration of another service (MCEDT).

Changes:

  • Changed clientKeystore from static to instance variable in EdtClientBuilder, allowing each service to maintain its own independent keystore configuration
  • Updated setClientKeystoreFilename() from static to instance method to support per-instance configuration
  • Modified all call sites (DelegateFactory, OnlineHCValidator, EDTBaseTest) to pass the builder instance to the keystore configuration method
  • Enhanced documentation from C-style comments to proper JavaDoc with detailed parameter descriptions and context

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/main/java/ca/openosp/openo/integration/ebs/client/ng/EdtClientBuilder.java Converted clientKeystore from static to instance variable and updated setClientKeystoreFilename() to instance method with improved JavaDoc
src/main/java/ca/openosp/openo/integration/mcedt/DelegateFactory.java Updated to pass builder instance to setExternalClientKeystoreFilename() and enhanced method documentation
src/main/java/ca/openosp/openo/integration/mchcv/OnlineHCValidator.java Updated to pass HCV builder instance to setExternalClientKeystoreFilename() and enhanced method documentation
src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java Updated test helper method to accept builder parameter and enhanced documentation

@claude
Copy link
Copy Markdown

claude bot commented Jan 29, 2026

Code Review

❌ CLAUDE.md Compliance Issue: Incorrect @SInCE Dates

The @since tags use 2025-08-26, which violates CLAUDE.md line 219:

@SInCE Tags: Use git history to determine accurate dates: git log --follow --format="%ai" <file> | tail -1

Affected locations:

Required fix: Change all instances of @since 2025-08-26 to @since 2026-01-29 (the actual date from git history when these changes were introduced).


✅ Code Quality: No Bugs Found

The implementation correctly fixes the race condition by converting clientKeystore from a static variable to an instance variable. All callers are properly updated to pass the builder instance. No logic errors or security issues detected.


Generated with Claude Code

@D3V41
Copy link
Copy Markdown
Collaborator Author

D3V41 commented Jan 29, 2026

@claude Will you commit the suggestion you gave about the date? Also, please follow all the Sourcery suggestions in separate commits.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 29, 2026

Claude finished @D3V41's task in 2m 56s —— View job


Tasks Completed ✅


Summary

I've successfully implemented all requested changes in 4 separate commits:

Commit 1: Documentation Fix (94cebabc)

  • Corrected @since dates from 2025-08-26 to 2026-01-29 across all 4 files
  • Aligns with CLAUDE.md requirement to use accurate git history dates

Commit 2: Bug Fix (8b6c0bd2)

Commit 3: Regression Test (f2de8830)

  • Added test to verify keystore configuration isolation between builder instances
  • Uses reflection to confirm two builders maintain independent keystore paths
  • Would fail if clientKeystore were ever made static again
  • Addresses Sourcery suggestion Added development-drugref.sql #3

Commit 4: Test Coverage (73efa77a)

  • Added tests for null and non-existent path handling
  • Verifies documented fallback behavior to default keystore
  • Ensures method contract matches implementation
  • Addresses Sourcery suggestion Add missing method to ActionUtils #4

All changes have been pushed to the fix/mcedt-hcv-keystore-race-condition branch.


|

github-actions bot and others added 4 commits January 29, 2026 17:58
Per CLAUDE.md documentation standards, @SInCE tags must use accurate dates
from git history. Updated all keystore configuration method JavaDoc to reflect
the actual commit date of these changes.

Co-authored-by: Deval Italiya <D3V41@users.noreply.github.com>
…ration

Added validation to check for empty or whitespace-only strings in addition to
null checks. This prevents Files.exists() from resolving to the current directory
when an empty string is provided, which would cause authentication failures with
confusing error messages.

Addresses Sourcery code review comments on DelegateFactory.java and
OnlineHCValidator.java.

Co-authored-by: Deval Italiya <D3V41@users.noreply.github.com>
Added test to verify that EdtClientBuilder instances maintain independent
keystore configurations. This prevents regression to the previous race
condition where a static clientKeystore variable was shared across all
instances, causing MCEDT uploads to fail after HCV validation.

The test uses reflection to verify that two builder instances can be
configured with different keystore paths without interfering with each other.
If clientKeystore were ever made static again, this test would fail.

Addresses Sourcery code review suggestion for regression test coverage.

Co-authored-by: Deval Italiya <D3V41@users.noreply.github.com>
Added tests to verify that setExternalClientKeystoreFilename correctly handles
edge cases where the keystore path is null or points to a non-existent file.
Both cases should result in the default keystore being used, per the method's
documented contract.

These tests ensure that:
1. Null paths do not modify the builder's keystore configuration
2. Non-existent file paths do not modify the builder's keystore configuration
3. The behavior matches the JavaDoc promise of falling back to the default
   keystore at src/main/resources/clientKeystore.properties

Addresses Sourcery code review suggestion for test coverage of documented
fallback behavior.

Co-authored-by: Deval Italiya <D3V41@users.noreply.github.com>
@D3V41
Copy link
Copy Markdown
Collaborator Author

D3V41 commented Jan 29, 2026

@claude review

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 29, 2026

Claude finished @D3V41's task in 2m 13s —— View job


Code Review - Comprehensive Analysis

  • Read current state of all changed files
  • Review core race condition fix implementation
  • Verify previous review feedback was addressed
  • Check CLAUDE.md compliance (security, docs, tests)
  • Provide comprehensive review feedback

✅ Overall Assessment: APPROVED

This PR successfully fixes a critical race condition in EDT client keystore configuration and addresses all review feedback comprehensively. The implementation is sound, well-tested, and follows project standards.


📋 Review Summary

Core Fix (Commit 2320607)

Problem Identified: Static clientKeystore variable in EdtClientBuilder was shared across all instances, causing MCEDT uploads to fail after HCV validation overwrote the keystore path.

Solution Implemented: ✅ Correct

  • Changed clientKeystore from static to instance variable (line 104 in EdtClientBuilder.java)
  • Changed setClientKeystoreFilename() from static to instance method (line 368)
  • Updated all 3 call sites to pass builder instance:
    • DelegateFactory.java:79 - MCEDT service
    • OnlineHCValidator.java:70 - HCV service
    • EDTBaseTest.java:342 - Test helper

Impact: Each EDT service (MCEDT, HCV) now maintains independent keystore configuration, eliminating the race condition.


🎯 Follow-up Improvements (Commits 94cebab through 73efa77)

1. Documentation Fix (Commit 94cebab) ✅

What: Corrected @since dates from 2025-08-26 to 2026-01-29
Why: CLAUDE.md requires accurate dates from git history
Files: All 4 changed files
Status: Properly implemented per project standards

2. Bug Fix: Empty/Blank String Handling (Commit 8b6c0bd) ✅

What: Added clientKeystorePropertiesPath.trim().isEmpty() check
Where:

  • DelegateFactory.java:119
  • OnlineHCValidator.java:140

Why: Prevents Files.exists("") from resolving to current directory (.), which would cause confusing authentication failures
Status: Critical edge case properly handled

3. Regression Test (Commit f2de883) ✅

What: Test verifies keystore configuration isolation between builder instances
Location: EDTBaseTest.java:382-421
Method: Uses reflection to verify two builders have independent clientKeystore values
Coverage: Would fail if clientKeystore were ever made static again
Status: Excellent regression protection

4. Edge Case Tests (Commit 73efa77) ✅

What: Tests for null and non-existent path handling
Location: EDTBaseTest.java:428-483
Coverage:

  • setExternalClientKeystoreFilename_nullPath_usesDefaultKeystore() - Verifies null doesn't modify builder
  • setExternalClientKeystoreFilename_nonExistentPath_usesDefaultKeystore() - Verifies non-existent path doesn't modify builder

Status: Comprehensive edge case coverage matching JavaDoc contract


🔍 Code Quality Analysis

Security ✅

  • No PHI exposure: Keystore paths are configuration, not patient data
  • No XSS risk: No user-facing output of paths
  • No injection risk: File paths validated via Files.exists() before use
  • Error handling: MalformedURLException properly caught and logged

Documentation ✅

  • JavaDoc complete: All modified methods have comprehensive JavaDoc
  • @SInCE tags accurate: Now use correct 2026-01-29 date
  • Parameter descriptions: Specific types and purposes documented
  • Contract clarity: Fallback behavior to default keystore clearly documented

Testing ✅

  • Unit tests: 3 new tests covering:
    1. Instance isolation (regression protection)
    2. Null path handling (edge case)
    3. Non-existent path handling (edge case)
  • Test quality: Uses reflection appropriately, clear assertions, good naming
  • Coverage: All new code paths tested

CLAUDE.md Compliance ✅

  • Documentation standards: JavaDoc follows project conventions
  • @SInCE accuracy: Uses git history dates per requirement
  • Test framework: JUnit 4 tests in legacy test structure (appropriate for this module)
  • Package naming: Uses correct ca.openosp.openo.* namespace

🎨 Code Architecture

Design Pattern: Instance-Based Configuration ✅

The change from static to instance-based configuration follows sound OOP principles:

  • Encapsulation: Each builder owns its configuration
  • Isolation: No shared mutable state between instances
  • Thread-safety: Eliminates race condition without need for synchronization
  • Testability: Easier to test with isolated instances

Call Site Updates ✅

All three call sites properly updated to new pattern:

// Before (static - WRONG)
EdtClientBuilder.setClientKeystoreFilename(path);

// After (instance - CORRECT)
EdtClientBuilder builder = new EdtClientBuilder(config);
setExternalClientKeystoreFilename(builder, path);
// ... then builder.setClientKeystoreFilename(url);

📊 Test Coverage Analysis

Regression Protection

The clientKeystoreConfigurationIsIsolatedPerBuilderInstance() test is particularly valuable:

  • Creates two independent builder instances
  • Configures different keystore paths on each
  • Uses reflection to verify field values are independent
  • Would immediately fail if someone reverted to static field

Edge Case Coverage

Both new edge case tests verify documented contract:

  • Null path → no modification (use default)
  • Non-existent path → no modification (use default)
  • Both use reflection to verify internal state unchanged

🔧 Minor Observations (Non-blocking)

1. Test File Location

Note: The test EDTBaseTest.java:362 is missing the trim().isEmpty() check that was added to the production code in DelegateFactory.java and OnlineHCValidator.java.

Location: EDTBaseTest.java:362

if (clientKeystorePropertiesPath == null) {
    return;
}

Should be:

if (clientKeystorePropertiesPath == null || clientKeystorePropertiesPath.trim().isEmpty()) {
    return;
}

Impact: Low - test helper may not handle empty strings the same way as production code
Recommendation: Add the same trim().isEmpty() check for consistency

2. Code Duplication (As noted by Sourcery)

The three setExternalClientKeystoreFilename() helper methods are nearly identical:

  • DelegateFactory.java:118-131
  • OnlineHCValidator.java:139-152
  • EDTBaseTest.java:361-374

Current state: Acceptable - each is in a different context (MCEDT service factory, HCV validator, test base)
Future enhancement: Could be extracted to a common utility, but current duplication is minimal and clear


✨ Strengths

  1. Root cause identified correctly: Static variable shared across services
  2. Minimal invasive fix: Changed only what was necessary
  3. Comprehensive testing: Regression test + edge cases
  4. Documentation quality: Clear JavaDoc explaining per-instance behavior
  5. Proper validation: Empty/blank string check prevents subtle bugs
  6. Clean commit history: Logical separation of concerns across commits

🏁 Final Recommendation

APPROVED

This PR:

  • ✅ Fixes the critical race condition correctly
  • ✅ Addresses all Sourcery review feedback
  • ✅ Addresses all previous @claude review feedback
  • ✅ Follows CLAUDE.md project standards
  • ✅ Includes comprehensive test coverage
  • ✅ Maintains backward compatibility

Minor follow-up (non-blocking): Consider adding trim().isEmpty() check to EDTBaseTest.java:362 for consistency with production code.


Files Reviewed

  • src/main/java/ca/openosp/openo/integration/ebs/client/ng/EdtClientBuilder.java - Core fix
  • src/main/java/ca/openosp/openo/integration/mcedt/DelegateFactory.java - MCEDT integration
  • src/main/java/ca/openosp/openo/integration/mchcv/OnlineHCValidator.java - HCV integration
  • src/test/java/ca/openosp/openo/integration/mcedt/EDTBaseTest.java - Test coverage

@yingbull yingbull self-requested a review January 30, 2026 00:46
@yingbull yingbull merged commit b5fa326 into hotfix/01232026 Jan 30, 2026
15 checks passed
@lacarmen lacarmen deleted the fix/mcedt-hcv-keystore-race-condition branch March 5, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: MCEDT upload fails after HCV validation due to shared static clientKeystore variable

4 participants