Honor user-cookie to select a temporal feature set 88/100288/6
authorIttay Stern <ittay.stern@att.com>
Tue, 14 Jan 2020 18:35:51 +0000 (20:35 +0200)
committerIttay Stern <ittay.stern@att.com>
Wed, 15 Jan 2020 13:42:46 +0000 (15:42 +0200)
Issue-ID: VID-747
Change-Id: I5acd4685bedfed8570a6fd698101f541d03c8d82
Signed-off-by: Ittay Stern <ittay.stern@att.com>
vid-app-common/src/main/java/org/onap/vid/properties/FeatureSetsManager.kt [new file with mode: 0644]
vid-app-common/src/main/java/org/onap/vid/properties/FeaturesTogglingConfiguration.java
vid-app-common/src/test/java/org/onap/vid/properties/FeatureSetsManagerTest.kt [new file with mode: 0644]
vid-app-common/src/test/resources/example.features.properties [new file with mode: 0644]

diff --git a/vid-app-common/src/main/java/org/onap/vid/properties/FeatureSetsManager.kt b/vid-app-common/src/main/java/org/onap/vid/properties/FeatureSetsManager.kt
new file mode 100644 (file)
index 0000000..207bde7
--- /dev/null
@@ -0,0 +1,82 @@
+package org.onap.vid.properties
+
+import org.apache.commons.io.filefilter.WildcardFileFilter
+import org.springframework.web.context.request.RequestContextHolder.getRequestAttributes
+import org.springframework.web.context.request.ServletRequestAttributes
+import org.togglz.core.Feature
+import org.togglz.core.manager.FeatureManager
+import org.togglz.core.manager.FeatureManagerBuilder
+import org.togglz.core.repository.file.FileBasedStateRepository
+import java.io.File
+import java.io.FilenameFilter
+import javax.servlet.ServletContext
+import javax.servlet.http.HttpServletRequest
+
+
+private const val SLOW_RELOAD = 60_000
+private const val COOKIE_NAME = "features.set"
+
+class FeatureSetsManager(
+        private val defaultManager: FeatureManager,
+        private val nameProvider: AlternativeFeatureSetNameProvider,
+        private val servletContext: ServletContext
+) : FeatureManager by defaultManager {
+
+    override fun isActive(feature: Feature?): Boolean {
+        return resolvedFeatureManager().isActive(feature)
+    }
+
+    private fun resolvedFeatureManager(): FeatureManager {
+        return when (val alternativeFeatureSetName = nameProvider.alternativeFeatureSetName) {
+            null -> defaultManager
+            else -> allFeatureManagers.getValue(alternativeFeatureSetName)
+        }
+    }
+
+    internal val allFeatureManagers: Map<String, FeatureManager> by lazy {
+        allFeatureSetFiles().associateBy(
+                { it.name },
+                { newFeatureManager(it) }
+        ).withDefault { allFeaturesOff }
+    }
+
+    private val allFeaturesOff =
+            FeatureManagerBuilder().featureEnum(Features::class.java).build()
+
+    private fun newFeatureManager(file: File): FeatureManager {
+        return FeatureManagerBuilder()
+                .featureEnum(Features::class.java)
+                .stateRepository(FileBasedStateRepository(file, SLOW_RELOAD))
+                .build()
+    }
+
+    private fun allFeatureSetFiles(): Array<File> {
+        val dir = File(servletContext.getRealPath("/WEB-INF/conf/"))
+        val fileFilter: FilenameFilter = WildcardFileFilter("*.features.properties")
+
+        return dir.listFiles(fileFilter) ?: emptyArray()
+    }
+}
+
+interface AlternativeFeatureSetNameProvider {
+    val alternativeFeatureSetName: String?
+}
+
+class AlternativeFeatureSetNameFromCookie: AlternativeFeatureSetNameProvider {
+    override val alternativeFeatureSetName: String?
+        get() = valueFromCookie(currentHttpRequest())
+
+    internal fun valueFromCookie(httpServletRequest: HttpServletRequest?): String? {
+        return httpServletRequest
+                ?.cookies
+                ?.firstOrNull { it.name == COOKIE_NAME }
+                ?.value
+    }
+
+    internal fun currentHttpRequest(): HttpServletRequest? {
+        return when (val requestAttributes = getRequestAttributes()) {
+            is ServletRequestAttributes -> requestAttributes.request
+            else -> null
+        }
+    }
+}
index 4d4387d..96331b7 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.onap.vid.properties;
 
+import java.io.File;
+import javax.servlet.ServletContext;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.annotation.Bean;
@@ -30,9 +32,6 @@ import org.togglz.core.manager.FeatureManagerBuilder;
 import org.togglz.core.repository.file.FileBasedStateRepository;
 import org.togglz.spring.listener.TogglzApplicationContextBinderApplicationListener;
 
-import javax.servlet.ServletContext;
-import java.io.File;
-
 @Configuration
 public class FeaturesTogglingConfiguration {
     @Bean
@@ -52,11 +51,13 @@ public class FeaturesTogglingConfiguration {
 
         filename = StringUtils.trimToNull(filename);
 
-        return new FeatureManagerBuilder()
+        return new FeatureSetsManager(
+            new FeatureManagerBuilder()
                 .featureEnum(Features.class)
                 .stateRepository(new FileBasedStateRepository(
-                        new File(filename.startsWith("/")? filename : servletContext.getRealPath("/WEB-INF/conf/" + filename))
+                    new File(filename.startsWith("/") ? filename : servletContext.getRealPath("/WEB-INF/conf/" + filename))
                 ))
-                .build();
+                .build(), new AlternativeFeatureSetNameFromCookie(), servletContext
+        );
     }
 }
diff --git a/vid-app-common/src/test/java/org/onap/vid/properties/FeatureSetsManagerTest.kt b/vid-app-common/src/test/java/org/onap/vid/properties/FeatureSetsManagerTest.kt
new file mode 100644 (file)
index 0000000..9cf7aa6
--- /dev/null
@@ -0,0 +1,101 @@
+package org.onap.vid.properties
+
+import org.hamcrest.CoreMatchers.*
+import org.hamcrest.MatcherAssert.assertThat
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.InjectMocks
+import org.mockito.Mock
+import org.mockito.Mockito.*
+import org.onap.vid.testUtils.TestUtils
+import org.springframework.web.context.request.RequestContextHolder
+import org.testng.annotations.BeforeMethod
+import org.testng.annotations.Test
+import org.togglz.core.manager.FeatureManager
+import javax.servlet.ServletContext
+import javax.servlet.http.Cookie
+import javax.servlet.http.HttpServletRequest
+import org.hamcrest.CoreMatchers.`is` as _is
+import org.mockito.Mockito.`when` as _when
+
+class FeatureSetsManagerTest {
+    @Mock
+    lateinit var defaultFeatureManager: FeatureManager
+    @Mock
+    lateinit var servletContext: ServletContext
+    @Mock
+    lateinit var alternativeFeatureSetNameProvider: AlternativeFeatureSetNameProvider
+    @InjectMocks
+    lateinit var featureSetsManager: FeatureSetsManager
+
+    private val alternativeFeatureSetNameFromCookie = AlternativeFeatureSetNameFromCookie()
+
+    @BeforeMethod
+    fun setUp() {
+        TestUtils.initMockitoMocks(this)
+    }
+
+    @Test
+    fun `isActive - without alternative features set name - delegates to default and no file loaded`() {
+        _when(defaultFeatureManager.isActive(Features.FLAG_1810_AAI_LOCAL_CACHE)).thenReturn(true)
+        _when(alternativeFeatureSetNameProvider.alternativeFeatureSetName).thenReturn(null)
+
+        assertThat(featureSetsManager.isActive(Features.FLAG_1810_AAI_LOCAL_CACHE), _is(true))
+
+        verifyZeroInteractions(servletContext) // implies no other file loaded
+        verify(defaultFeatureManager, times(1)).isActive(Features.FLAG_1810_AAI_LOCAL_CACHE)
+    }
+
+    @Test
+    fun `isActive - with alternative features set - brings flags from alternative`() {
+        _when(servletContext.getRealPath(anyString())).thenReturn(this.javaClass.getResource("/").path)
+        _when(alternativeFeatureSetNameProvider.alternativeFeatureSetName).thenReturn("example.features.properties")
+
+        assertThat(featureSetsManager.isActive(Features.FLAG_1810_AAI_LOCAL_CACHE), _is(true))
+        assertThat(featureSetsManager.isActive(Features.FLAG_1902_NEW_VIEW_EDIT), _is(false))
+        verifyZeroInteractions(defaultFeatureManager)
+    }
+
+    @Test
+    fun `isActive - with non-existing alternative features set - fallback is to all flags off`() {
+        _when(servletContext.getRealPath(anyString())).thenReturn(this.javaClass.getResource("/").path)
+        _when(alternativeFeatureSetNameProvider.alternativeFeatureSetName).thenReturn("non-existing")
+
+        assertThat(featureSetsManager, not(nullValue()))
+        assertThat(
+                featureSetsManager.features.map { featureSetsManager.isActive(it) },
+                not(hasItem(true))
+        )
+    }
+
+    @Test
+    fun `valueFromCookie - given no request - return null`() {
+        assertThat(alternativeFeatureSetNameFromCookie.valueFromCookie(null), _is(nullValue()))
+    }
+
+    @Test
+    fun `valueFromCookie - given request - return the correct cookie value`() {
+        val servletRequestMock = mock(HttpServletRequest::class.java)
+        _when(servletRequestMock.cookies).thenReturn(arrayOf(Cookie("features.set", "value")))
+
+        assertThat(alternativeFeatureSetNameFromCookie.valueFromCookie(servletRequestMock), _is("value"))
+    }
+
+    @Test
+    fun `valueFromCookie - given request without cookies - return null`() {
+        val servletRequestMock = mock(HttpServletRequest::class.java)
+        _when(servletRequestMock.cookies).thenReturn(emptyArray())
+
+        assertThat(alternativeFeatureSetNameFromCookie.valueFromCookie(servletRequestMock), _is(nullValue()))
+    }
+
+    @Test
+    fun `currentHttpRequest - when no current request - return null`() {
+        assertPrecondition()
+        assertThat(alternativeFeatureSetNameFromCookie.currentHttpRequest(), _is(nullValue()))
+    }
+
+    private fun assertPrecondition() {
+        assertThat("precondition for test not met: static RequestContextHolder.getRequestAttributes should be null",
+                RequestContextHolder.getRequestAttributes(), _is(nullValue()))
+    }
+}
diff --git a/vid-app-common/src/test/resources/example.features.properties b/vid-app-common/src/test/resources/example.features.properties
new file mode 100644 (file)
index 0000000..e8ac3eb
--- /dev/null
@@ -0,0 +1 @@
+FLAG_1810_AAI_LOCAL_CACHE=true
\ No newline at end of file