diff --git a/Jenkinsfile_CNP b/Jenkinsfile_CNP index 3e20cfc1..e284255b 100644 --- a/Jenkinsfile_CNP +++ b/Jenkinsfile_CNP @@ -17,13 +17,17 @@ GradleBuilder builder = new GradleBuilder(this, product) def nonPrSecrets = [ 'reform-scan-${env}': [ secret('notification-staging-queue-send-shared-access-key', 'NOTIFICATION_QUEUE_ACCESS_KEY_WRITE'), - secret('test-s2s-secret', 'TEST_S2S_SECRET') + secret('test-s2s-secret', 'TEST_S2S_SECRET'), + secret('launch-darkly-sdk-key', 'LAUNCH_DARKLY_SDK_KEY'), + secret('launch-darkly-offline-mode', 'LAUNCH_DARKLY_OFFLINE_MODE') ] ] def prSecrets = [ 'reform-scan-${env}': [ - secret('test-s2s-secret', 'TEST_S2S_SECRET') + secret('test-s2s-secret', 'TEST_S2S_SECRET'), + secret('launch-darkly-sdk-key', 'LAUNCH_DARKLY_SDK_KEY'), + secret('launch-darkly-offline-mode', 'LAUNCH_DARKLY_OFFLINE_MODE') ] ] diff --git a/Jenkinsfile_nightly b/Jenkinsfile_nightly index d9c5d849..5ae259e4 100644 --- a/Jenkinsfile_nightly +++ b/Jenkinsfile_nightly @@ -14,7 +14,9 @@ def component = "notification-service" def secrets = [ 'reform-scan-${env}': [ secret('fortify-on-demand-username', 'FORTIFY_USER_NAME'), - secret('fortify-on-demand-password', 'FORTIFY_PASSWORD') + secret('fortify-on-demand-password', 'FORTIFY_PASSWORD'), + secret('launch-darkly-sdk-key', 'LAUNCH_DARKLY_SDK_KEY'), + secret('launch-darkly-offline-mode', 'LAUNCH_DARKLY_OFFLINE_MODE') ] ] diff --git a/build.gradle b/build.gradle index 7faf36e2..eb5664de 100644 --- a/build.gradle +++ b/build.gradle @@ -236,6 +236,8 @@ dependencies { implementation group: 'org.apache.qpid', name: 'qpid-jms-client', version: '1.11.0' + implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '6.3.0' + testImplementation libraries.junit5 testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', { exclude group: 'junit', module: 'junit' @@ -257,6 +259,7 @@ dependencies { integrationTestImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.19.3' smokeTestImplementation sourceSets.main.runtimeClasspath + smokeTestImplementation sourceSets.test.runtimeClasspath smokeTestImplementation libraries.junit5 smokeTestImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' smokeTestImplementation group: 'com.typesafe', name: 'config', version: '1.4.3' diff --git a/charts/reform-scan-notification-service/Chart.yaml b/charts/reform-scan-notification-service/Chart.yaml index 7a8ee92c..6d567023 100644 --- a/charts/reform-scan-notification-service/Chart.yaml +++ b/charts/reform-scan-notification-service/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 description: A Helm chart for Reform Scan Notification Service name: reform-scan-notification-service home: https://github.com/hmcts/reform-scan-notification-service -version: 2.0.9 +version: 2.0.10 maintainers: - name: HMCTS BSP Team email: bspteam@hmcts.net diff --git a/charts/reform-scan-notification-service/values.yaml b/charts/reform-scan-notification-service/values.yaml index 896f1a85..42b98665 100644 --- a/charts/reform-scan-notification-service/values.yaml +++ b/charts/reform-scan-notification-service/values.yaml @@ -28,6 +28,10 @@ java: alias: QUEUE_READ_ACCESS_KEY - name: app-insights-connection-string alias: app-insights-connection-string + - name: launch-darkly-sdk-key + alias: LAUNCH_DARKLY_SDK_KEY + - name: launch-darkly-offline-mode + alias: LAUNCH_DARKLY_OFFLINE_MODE environment: DB_CONN_OPTIONS: ?sslmode=require FLYWAY_SKIP_MIGRATIONS: true diff --git a/infrastructure/main.tf b/infrastructure/main.tf index 8df189c0..973a0699 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -119,3 +119,14 @@ resource "azurerm_key_vault_secret" "test_s2s_secret" { } # endregion + +# Create secrets for Launch darkly - values manually populated +data "azurerm_key_vault_secret" "launch_darkly_sdk_key" { + name = "launch-darkly-sdk-key" + key_vault_id = data.azurerm_key_vault.reform_scan_key_vault.id +} + +data "azurerm_key_vault_secret" "launch_darkly_offline_mode" { + name = "launch-darkly-offline-mode" + key_vault_id = data.azurerm_key_vault.reform_scan_key_vault.id +} \ No newline at end of file diff --git a/src/integrationTest/resources/application.properties b/src/integrationTest/resources/application.properties index 4e2ce455..3454118a 100644 --- a/src/integrationTest/resources/application.properties +++ b/src/integrationTest/resources/application.properties @@ -16,4 +16,3 @@ scheduling.task.pending-notifications.send-delay-in-minute=60 scheduling.task.notifications-consume.enabled=false scheduling.task.notifications-consume.check.delay=1000000 idam.s2s-auth.url=false - diff --git a/src/main/java/uk/gov/hmcts/reform/notificationservice/controller/FeatureFlagController.java b/src/main/java/uk/gov/hmcts/reform/notificationservice/controller/FeatureFlagController.java new file mode 100644 index 00000000..f463e6bb --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/notificationservice/controller/FeatureFlagController.java @@ -0,0 +1,24 @@ +package uk.gov.hmcts.reform.notificationservice.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import uk.gov.hmcts.reform.notificationservice.launchdarkly.LaunchDarklyClient; + +import static org.springframework.http.ResponseEntity.ok; + +@RestController +public class FeatureFlagController { + private final LaunchDarklyClient featureToggleService; + + public FeatureFlagController(LaunchDarklyClient featureToggleService) { + this.featureToggleService = featureToggleService; + } + + @GetMapping("/feature-flags/{flag}") + public ResponseEntity flagStatus(@PathVariable String flag) { + boolean isEnabled = featureToggleService.isFeatureEnabled(flag); + return ok(flag + " : " + isEnabled); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/Flags.java b/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/Flags.java new file mode 100644 index 00000000..f26d0379 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/Flags.java @@ -0,0 +1,9 @@ +package uk.gov.hmcts.reform.notificationservice.launchdarkly; + +public final class Flags { + private Flags() { + + } + + public static final String REFORM_SCAN_NOTIFICATION_SERVICE_TEST = "reform-scan-notification-service-test"; +} diff --git a/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClient.java b/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClient.java new file mode 100644 index 00000000..5db59a76 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClient.java @@ -0,0 +1,41 @@ +package uk.gov.hmcts.reform.notificationservice.launchdarkly; + + +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; +import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class LaunchDarklyClient { + public static final LDUser REFORM_SCAN_NOTIFICATION_SERVICE_USER = new LDUser.Builder("reform-scan-notification" + + "-service") + .anonymous(true) + .build(); + + private final LDClientInterface internalClient; + + @Autowired + public LaunchDarklyClient( + LaunchDarklyClientFactory launchDarklyClientFactory, + @Value("${launchdarkly.sdk-key:YYYYY}") String sdkKey, + @Value("${launchdarkly.offline-mode:false}") Boolean offlineMode + ) { + this.internalClient = launchDarklyClientFactory.create(sdkKey, offlineMode); + } + + public boolean isFeatureEnabled(String feature) { + return internalClient.boolVariation(feature, LaunchDarklyClient.REFORM_SCAN_NOTIFICATION_SERVICE_USER, + false); + } + + public boolean isFeatureEnabled(String feature, LDUser user) { + return internalClient.boolVariation(feature, user, false); + } + + public DataSourceStatusProvider.Status getDataSourceStatus() { + return internalClient.getDataSourceStatusProvider().getStatus(); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientFactory.java b/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientFactory.java new file mode 100644 index 00000000..94c1b88b --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientFactory.java @@ -0,0 +1,16 @@ +package uk.gov.hmcts.reform.notificationservice.launchdarkly; + +import com.launchdarkly.sdk.server.LDClient; +import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import org.springframework.stereotype.Service; + +@Service +public class LaunchDarklyClientFactory { + public LDClientInterface create(String sdkKey, boolean offlineMode) { + LDConfig config = new LDConfig.Builder() + .offline(offlineMode) + .build(); + return new LDClient(sdkKey, config); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e544accf..e9e6456e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -69,3 +69,8 @@ jms: enabled: ${JMS_ENABLED:false} # end of clients region + +launchdarkly: + sdk-key: ${LAUNCH_DARKLY_SDK_KEY:XXXXX} + offline-mode: ${LAUNCH_DARKLY_OFFLINE_MODE:false} + diff --git a/src/smokeTest/java/uk/gov/hmcts/reform/notificationservice/LaunchDarklySmokeTest.java b/src/smokeTest/java/uk/gov/hmcts/reform/notificationservice/LaunchDarklySmokeTest.java new file mode 100644 index 00000000..4b886048 --- /dev/null +++ b/src/smokeTest/java/uk/gov/hmcts/reform/notificationservice/LaunchDarklySmokeTest.java @@ -0,0 +1,42 @@ +package uk.gov.hmcts.reform.notificationservice; + +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import uk.gov.hmcts.reform.notificationservice.launchdarkly.LaunchDarklyClient; +import uk.gov.hmcts.reform.notificationservice.launchdarkly.LaunchDarklyClientFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestPropertySource("classpath:application.conf") +@ExtendWith(SpringExtension.class) +class LaunchDarklySmokeTest { + + @MockBean + private LaunchDarklyClient ldClient; + @MockBean + private LaunchDarklyClientFactory ldFactory; + + @Value("${sdk-key:YYYYY}") + private String sdkKey; + + @Value("${offline-mode:false}") + private Boolean offlineMode; + + @BeforeEach + void setUp() { + ldFactory = new LaunchDarklyClientFactory(); + ldClient = new LaunchDarklyClient(ldFactory, sdkKey, offlineMode); + } + + @Test + void checkLaunchDarklyStatus() { + DataSourceStatusProvider.Status ldStatus = ldClient.getDataSourceStatus(); + assertThat(ldStatus.getState()).isEqualTo(DataSourceStatusProvider.State.VALID); + } +} diff --git a/src/smokeTest/java/uk/gov/hmcts/reform/notificationservice/NotificationServiceHealthTest.java b/src/smokeTest/java/uk/gov/hmcts/reform/notificationservice/NotificationServiceHealthTest.java deleted file mode 100644 index 01536182..00000000 --- a/src/smokeTest/java/uk/gov/hmcts/reform/notificationservice/NotificationServiceHealthTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package uk.gov.hmcts.reform.notificationservice; - -import com.typesafe.config.ConfigFactory; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.health.Status; -import org.springframework.http.HttpStatus; - -import static org.hamcrest.Matchers.equalTo; - -class NotificationServiceHealthTest { - - private static final String TEST_URL = ConfigFactory.load().getString("test-url"); - - @Test - void notification_service_is_healthy() { - // TODO: this test is there so that a test report can be created for smoke tests - // (otherwise the build fails). Remove when actual smoke tests have been written. - RestAssured - .given() - .relaxedHTTPSValidation() - .baseUri(TEST_URL) - .get("/health") - .then() - .assertThat() - .statusCode(HttpStatus.OK.value()) - .and() - .body("status", equalTo(Status.UP.toString())); - } -} diff --git a/src/smokeTest/resources/application.conf b/src/smokeTest/resources/application.conf index 3aac6298..46d556ec 100644 --- a/src/smokeTest/resources/application.conf +++ b/src/smokeTest/resources/application.conf @@ -1,2 +1,4 @@ test-url = "http://localhost:8583" test-url = ${?TEST_URL} +offline-mode=${LAUNCH_DARKLY_OFFLINE_MODE} +sdk-key=${LAUNCH_DARKLY_SDK_KEY} diff --git a/src/test/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientFactoryTest.java b/src/test/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientFactoryTest.java new file mode 100644 index 00000000..78514deb --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientFactoryTest.java @@ -0,0 +1,25 @@ +package uk.gov.hmcts.reform.notificationservice.launchdarkly; + +import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +class LaunchDarklyClientFactoryTest { + private LaunchDarklyClientFactory factory; + + @BeforeEach + void setUp() { + factory = new LaunchDarklyClientFactory(); + } + + @Test + void testCreate() throws IOException { + try (LDClientInterface client = factory.create("test key", true)) { + assertThat(client).isNotNull(); + } + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientTest.java b/src/test/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientTest.java new file mode 100644 index 00000000..9d3edd3a --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/notificationservice/launchdarkly/LaunchDarklyClientTest.java @@ -0,0 +1,63 @@ +package uk.gov.hmcts.reform.notificationservice.launchdarkly; + +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LaunchDarklyClientTest { + private static final String SDK_KEY = "fake-key"; + private static final String FAKE_FEATURE = "fake-feature"; + + @Mock + private LaunchDarklyClientFactory launchDarklyClientFactory; + + @Mock + private LDClientInterface ldClient; + + @Mock + private LDUser ldUser; + + private LaunchDarklyClient launchDarklyClient; + + @BeforeEach + void setUp() { + when(launchDarklyClientFactory.create(eq(SDK_KEY), anyBoolean())).thenReturn(ldClient); + launchDarklyClient = new LaunchDarklyClient(launchDarklyClientFactory, SDK_KEY, true); + } + + @Test + void testFeatureEnabled() { + when(ldClient.boolVariation(eq(FAKE_FEATURE), any(LDUser.class), anyBoolean())).thenReturn(true); + assertTrue(launchDarklyClient.isFeatureEnabled(FAKE_FEATURE, ldUser)); + } + + @Test + void testFeatureDisabled() { + when(ldClient.boolVariation(eq(FAKE_FEATURE), any(LDUser.class), anyBoolean())).thenReturn(false); + assertFalse(launchDarklyClient.isFeatureEnabled(FAKE_FEATURE, ldUser)); + } + + @Test + void testFeatureEnabledWithoutUser() { + when(ldClient.boolVariation(eq(FAKE_FEATURE), any(LDUser.class), anyBoolean())).thenReturn(true); + assertTrue(launchDarklyClient.isFeatureEnabled(FAKE_FEATURE)); + } + + @Test + void testFeatureDisabledWithoutUser() { + when(ldClient.boolVariation(eq(FAKE_FEATURE), any(LDUser.class), anyBoolean())).thenReturn(false); + assertFalse(launchDarklyClient.isFeatureEnabled(FAKE_FEATURE)); + } +}