chore: remove FF4J (#28653)

## Description
> Remove FF4J 

#### PR fixes following issue(s)
Fixes #24872 
> if no issue exists, please create an issue and ask the maintainers
about this first
>
>
#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
- Chore (housekeeping or task changes that don't impact user perception)

## Testing
>
#### How Has This Been Tested?
- [ ] Manual
- [x] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed


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

- **Refactor**
- Simplified the feature flag checking process, removing dependencies on
external libraries.
- Improved error handling in feature flag services for more robust
operation.

- **Bug Fixes**
- Adjusted feature flag refresh logic to ensure accurate status
representation.

- **Tests**
- Updated test cases to align with the new feature flag checking logic
and error handling improvements.

- **Documentation**
- Removed outdated comments and documentation related to the old feature
flag system.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Nilesh Sarupriya <20905988+nsarupr@users.noreply.github.com>
Co-authored-by: Abhijeet <abhi.nagarnaik@gmail.com>
This commit is contained in:
Nilesh Sarupriya 2023-12-11 23:14:58 -06:00 committed by GitHub
parent 9632bca064
commit 165856e885
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 79 additions and 399 deletions

View File

@ -1,17 +0,0 @@
package com.appsmith.server.configurations;
import org.ff4j.FF4j;
import org.ff4j.conf.XmlParser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeatureFlagConfig {
// Since we rely on cloud services to retrieve the most up-to-date flags, we avoid automatically generating the same
// flag in the FF4J context.
@Bean
public FF4j ff4j() {
return new FF4j(new XmlParser(), "features/init-flags.xml").audit(true).autoCreate(false);
}
}

View File

@ -1,19 +1,5 @@
package com.appsmith.server.featureflags;
import org.ff4j.core.FlippingStrategy;
import org.ff4j.strategy.PonderationStrategy;
import org.ff4j.strategy.time.OfficeHourStrategy;
/**
* This enum lists all the feature flags available along with their flipping strategy.
* In order to create a new feature flag, create another enum entry and add the same string to {@link features/init-flags.xml}
* <p>
* If you wish to define a custom flipping strategy, define a class that implements {@link FlippingStrategy} and
* ensure that you've mentioned this custom class when defining the feature in {@link features/init-flags.xml}
* <p>
* The feature flag implementation class should extend an existing feature flag implementation like {@link PonderationStrategy},
* {@link OfficeHourStrategy} etc. These default classes provide a lot of basic functionality out of the box.
*/
public enum FeatureFlagEnum {
// ------------------- These features are only for JUnit testing. DO NOT use these features in your code path.--- //
// ------------------- Couldn't find a better way to do this ---------------------------------------------------- //

View File

@ -1,24 +0,0 @@
package com.appsmith.server.featureflags.strategies;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.ff4j.core.FeatureStore;
import org.ff4j.core.FlippingExecutionContext;
import org.ff4j.strategy.AbstractFlipStrategy;
/**
* This strategy enables a given feature for Appsmith users only. Useful when features are under development and not
* ready for prime-time
*/
@Slf4j
public class AppsmithUserStrategy extends AbstractFlipStrategy {
@Override
public boolean evaluate(String featureName, FeatureStore store, FlippingExecutionContext executionContext) {
User user = (User) executionContext.getValue(FieldName.USER, true);
return StringUtils.endsWith(user.getEmail(), "@appsmith.com");
}
}

View File

@ -1,63 +0,0 @@
package com.appsmith.server.featureflags.strategies;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.ff4j.core.FeatureStore;
import org.ff4j.core.FlippingExecutionContext;
import org.ff4j.strategy.AbstractFlipStrategy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Slf4j
public class EmailBasedRolloutStrategy extends AbstractFlipStrategy {
List<String> validDomains = new ArrayList<>();
List<String> validEmails = new ArrayList<>();
private static final String PARAM_EMAIL_DOMAINS = "emailDomains";
private static final String PARAM_EMAILS = "emails";
/**
* {@inheritDoc}
*/
@Override
public void init(String featureName, Map<String, String> initParam) {
super.init(featureName, initParam);
if (!initParam.containsKey(PARAM_EMAIL_DOMAINS) && !initParam.containsKey(PARAM_EMAILS)) {
String msg = String.format(
"Either '%s' or '%s' is required for EmailBasedRolloutStrategy", PARAM_EMAIL_DOMAINS, PARAM_EMAILS);
throw new IllegalArgumentException(msg);
}
if (!StringUtils.isEmpty(initParam.get(PARAM_EMAIL_DOMAINS))) {
this.validDomains = Arrays.asList(initParam.get(PARAM_EMAIL_DOMAINS).split(","));
}
if (!StringUtils.isEmpty(initParam.get(PARAM_EMAILS))) {
this.validEmails = Arrays.asList(initParam.get(PARAM_EMAILS).split(","));
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean evaluate(String featureName, FeatureStore store, FlippingExecutionContext executionContext) {
User user = (User) executionContext.getValue(FieldName.USER, true);
int atIndex = user.getEmail().indexOf("@");
if (atIndex > 0) {
// If the email domain is valid, check the user's email ID against the list of validated domains
String domain = user.getEmail().substring(atIndex + 1).toLowerCase();
if (validDomains.contains(domain)) {
return true;
} else {
return validEmails.contains(user.getEmail());
}
}
return false;
}
}

View File

@ -2,21 +2,18 @@ package com.appsmith.server.services;
import com.appsmith.server.helpers.FeatureFlagMigrationHelper;
import com.appsmith.server.services.ce.FeatureFlagServiceCEImpl;
import org.ff4j.FF4j;
import org.springframework.stereotype.Component;
@Component
public class FeatureFlagServiceImpl extends FeatureFlagServiceCEImpl implements FeatureFlagService {
public FeatureFlagServiceImpl(
SessionUserService sessionUserService,
FF4j ff4j,
TenantService tenantService,
UserIdentifierService userIdentifierService,
CacheableFeatureFlagHelper cacheableFeatureFlagHelper,
FeatureFlagMigrationHelper featureFlagMigrationHelper) {
super(
sessionUserService,
ff4j,
tenantService,
userIdentifierService,
cacheableFeatureFlagHelper,

View File

@ -152,7 +152,7 @@ public class CacheableFeatureFlagHelperCEImpl implements CacheableFeatureFlagHel
// We're gobbling up errors here so that all feature flags are turned off by default
// This will be problematic if we do not maintain code to reflect validity of flags
log.debug("Received error from CS for feature flags: {}", error.getMessage());
return Mono.just(Map.of());
return Mono.just(new HashMap<>());
});
}
@ -166,8 +166,14 @@ public class CacheableFeatureFlagHelperCEImpl implements CacheableFeatureFlagHel
public Mono<CachedFeatures> fetchCachedTenantFeatures(String tenantId) {
return this.forceAllRemoteFeaturesForTenant(tenantId).flatMap(flags -> {
CachedFeatures cachedFeatures = new CachedFeatures();
cachedFeatures.setRefreshedAt(Instant.now());
cachedFeatures.setFeatures(flags);
// If CS is down we expect the empty flags, from upstream method. Hence, setting the refreshed at to past
// so that the next call will have the force refresh.
if (CollectionUtils.isNullOrEmpty(flags)) {
cachedFeatures.setRefreshedAt(Instant.now().minus(1, ChronoUnit.DAYS));
} else {
cachedFeatures.setRefreshedAt(Instant.now());
}
return Mono.just(cachedFeatures);
});
}

View File

@ -1,7 +1,6 @@
package com.appsmith.server.services.ce;
import com.appsmith.server.domains.Tenant;
import com.appsmith.server.domains.User;
import com.appsmith.server.featureflags.CachedFeatures;
import com.appsmith.server.featureflags.FeatureFlagEnum;
import reactor.core.publisher.Mono;
@ -10,16 +9,6 @@ import java.util.Map;
public interface FeatureFlagServiceCE {
/**
* Used to check if a particular feature is enabled for a given user. Useful in contexts where we already have the
* User object and simply wish to do a boolean check
*
* @param featureEnum
* @param user
* @return Boolean
*/
Mono<Boolean> check(FeatureFlagEnum featureEnum, User user);
/**
* Check if a particular feature is enabled for the current logged in user. Useful in chaining reactive functions
* while writing business logic that may depend on a feature flag
@ -29,8 +18,6 @@ public interface FeatureFlagServiceCE {
*/
Mono<Boolean> check(FeatureFlagEnum featureEnum);
Boolean check(String featureName, User user);
/**
* Fetch all the flags and their values for the current logged in user
*

View File

@ -1,6 +1,5 @@
package com.appsmith.server.services.ce;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.constants.MigrationStatus;
import com.appsmith.server.domains.Tenant;
import com.appsmith.server.domains.TenantConfiguration;
@ -16,19 +15,13 @@ import com.appsmith.server.services.TenantService;
import com.appsmith.server.services.UserIdentifierService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ff4j.FF4j;
import org.ff4j.core.FlippingExecutionContext;
import org.ff4j.exception.FeatureNotFoundException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import static java.lang.Boolean.TRUE;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
@ -36,8 +29,6 @@ public class FeatureFlagServiceCEImpl implements FeatureFlagServiceCE {
private final SessionUserService sessionUserService;
private final FF4j ff4j;
private final TenantService tenantService;
private final UserIdentifierService userIdentifierService;
@ -49,26 +40,6 @@ public class FeatureFlagServiceCEImpl implements FeatureFlagServiceCE {
private CachedFeatures cachedTenantFeatureFlags;
private Mono<Boolean> checkAll(String featureName, User user) {
Boolean check = check(featureName, user);
if (TRUE.equals(check)) {
return Mono.just(true);
}
return getAllFeatureFlagsForUser()
.flatMap(featureMap -> Mono.justOrEmpty(featureMap.get(featureName)))
.switchIfEmpty(Mono.just(false));
}
@Override
public Mono<Boolean> check(FeatureFlagEnum featureEnum, User user) {
if (featureEnum == null) {
return Mono.just(false);
}
return checkAll(featureEnum.toString(), user);
}
/**
* This function checks if the feature is enabled for the current user. In case the user object is not present,
* i.e. when the method is getting called internally via cron or other mechanism check for tenant level flag and
@ -79,32 +50,11 @@ public class FeatureFlagServiceCEImpl implements FeatureFlagServiceCE {
*/
@Override
public Mono<Boolean> check(FeatureFlagEnum featureEnum) {
// Check if the feature is supported at the tenant level and provide a fallback as falsy value
// i.e. not supported
Mono<Boolean> isTenantFeatureSupported = this.getTenantFeatures()
.flatMap(featureMap -> Mono.justOrEmpty(featureMap.get(featureEnum.name())))
.switchIfEmpty(Mono.just(false));
return sessionUserService
.getCurrentUser()
.flatMap(user -> check(featureEnum, user))
.switchIfEmpty(isTenantFeatureSupported);
}
@Override
public Boolean check(String featureName, User user) {
try {
return ff4j.check(featureName, new FlippingExecutionContext(Map.of(FieldName.USER, user)));
} catch (Exception e) {
// FF4J is configured not to auto-generate a flag if it's not present in init-flags.xml
// (see FeatureFlagConfig.java).
// Consequently, we anticipate that the flag may not exist in the FF4J context and need to handle any
// related exceptions silently.
if (!(e instanceof FeatureNotFoundException)) {
log.error("Error checking feature flag: {}", featureName, e);
}
if (Objects.isNull(featureEnum)) {
return Mono.just(Boolean.FALSE);
}
return false;
return this.getAllFeatureFlagsForUser()
.map(featureMap -> featureMap.getOrDefault(featureEnum.name(), Boolean.FALSE));
}
/**
@ -115,27 +65,16 @@ public class FeatureFlagServiceCEImpl implements FeatureFlagServiceCE {
*/
@Override
public Mono<Map<String, Boolean>> getAllFeatureFlagsForUser() {
Mono<User> currentUser = sessionUserService.getCurrentUser().cache();
Flux<Tuple2<String, User>> featureUserTuple = Flux.fromIterable(
ff4j.getFeatures().keySet())
.flatMap(featureName -> Mono.just(featureName).zipWith(currentUser));
// Filter out anonymous users, then collect feature flags into a Map
Mono<Map<String, Boolean>> localFlagsForUser = featureUserTuple
.filter(objects -> !objects.getT2().isAnonymous())
.collectMap(Tuple2::getT1, tuple -> check(tuple.getT1(), tuple.getT2()));
// Combine local flags, remote flags, and tenant features, and merge them into a single map
return localFlagsForUser.flatMap(localFlags -> this.getAllRemoteFeatureFlagsForUser()
.zipWith(this.getTenantFeatures())
return Mono.zip(this.getAllRemoteFeatureFlagsForUser(), this.getTenantFeatures())
.map(remoteAndTenantFlags -> {
Map<String, Boolean> combinedFlags = new HashMap<>(localFlags);
Map<String, Boolean> combinedFlags = new HashMap<>();
combinedFlags.putAll(remoteAndTenantFlags.getT1());
// Always add the tenant level flags after the user flags to make sure tenant flags gets the
// precedence
combinedFlags.putAll(remoteAndTenantFlags.getT2());
return combinedFlags;
}));
});
}
/**
@ -146,33 +85,33 @@ public class FeatureFlagServiceCEImpl implements FeatureFlagServiceCE {
private Mono<Map<String, Boolean>> getAllRemoteFeatureFlagsForUser() {
Mono<User> userMono = sessionUserService.getCurrentUser().cache();
return userMono.flatMap(user -> {
String userIdentifier = userIdentifierService.getUserIdentifier(user);
// Checks for flags present in cache and if the cache is not expired
return cacheableFeatureFlagHelper
.fetchUserCachedFlags(userIdentifier, user)
.flatMap(cachedFlags -> {
if (cachedFlags.getRefreshedAt().until(Instant.now(), ChronoUnit.MINUTES)
< FEATURE_FLAG_CACHE_TIME_MIN) {
return Mono.just(cachedFlags.getFlags());
} else {
// empty the cache for the userIdentifier as expired
return cacheableFeatureFlagHelper
.evictUserCachedFlags(userIdentifier)
.then(cacheableFeatureFlagHelper.fetchUserCachedFlags(userIdentifier, user))
.flatMap(cachedFlagsUpdated -> {
// In case the retrieval of the latest flags from CS encounters an error, the
// previous flags will serve as a fallback value.
if (cachedFlagsUpdated == null
|| CollectionUtils.isNullOrEmpty(cachedFlagsUpdated.getFlags())) {
return cacheableFeatureFlagHelper
.updateUserCachedFlags(userIdentifier, cachedFlags)
.map(CachedFlags::getFlags);
}
return Mono.just(cachedFlagsUpdated.getFlags());
});
}
});
});
String userIdentifier = userIdentifierService.getUserIdentifier(user);
// Checks for flags present in cache and if the cache is not expired
return cacheableFeatureFlagHelper
.fetchUserCachedFlags(userIdentifier, user)
.flatMap(cachedFlags -> {
if (cachedFlags.getRefreshedAt().until(Instant.now(), ChronoUnit.MINUTES)
< FEATURE_FLAG_CACHE_TIME_MIN) {
return Mono.just(cachedFlags.getFlags());
}
// empty the cache for the userIdentifier as expired
return cacheableFeatureFlagHelper
.evictUserCachedFlags(userIdentifier)
.then(cacheableFeatureFlagHelper.fetchUserCachedFlags(userIdentifier, user))
.flatMap(cachedFlagsUpdated -> {
// In case the retrieval of the latest flags from CS encounters an error,
// the previous flags will serve as a fallback value.
if (cachedFlagsUpdated == null
|| CollectionUtils.isNullOrEmpty(cachedFlagsUpdated.getFlags())) {
return cacheableFeatureFlagHelper
.updateUserCachedFlags(userIdentifier, cachedFlags)
.map(CachedFlags::getFlags);
}
return Mono.just(cachedFlagsUpdated.getFlags());
});
});
})
.switchIfEmpty(Mono.just(new HashMap<>()));
}
/**
@ -223,7 +162,8 @@ public class FeatureFlagServiceCEImpl implements FeatureFlagServiceCE {
.map(cachedFeatures -> {
cachedTenantFeatureFlags = cachedFeatures;
return cachedFeatures.getFeatures();
});
})
.switchIfEmpty(Mono.just(new HashMap<>()));
}
/**

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ff4j>
<autocreate>false</autocreate>
<audit>false</audit>
<features>
<feature uid="APP_NAVIGATION_LOGO_UPLOAD" enable="true"
description="Logo upload feature for app viewer navigation">
<flipstrategy class="com.appsmith.server.featureflags.strategies.EmailBasedRolloutStrategy">
<param name="emailDomains" value="appsmith.com,moolya.com"/>
</flipstrategy>
</feature>
</features>
</ff4j>

View File

@ -4,6 +4,7 @@ import com.appsmith.server.aspect.component.TestComponent;
import com.appsmith.server.featureflags.CachedFeatures;
import com.appsmith.server.featureflags.FeatureFlagEnum;
import com.appsmith.server.services.FeatureFlagService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
@ -34,6 +35,16 @@ class FeatureFlaggedMethodInvokerAspectTest {
private static final String CE_COMPATIBLE_RESPONSE = "ce_compatible_impl_method";
private static final String CE_RESPONSE = "ce_impl_method";
@BeforeEach
void setUp() {
Mockito.when(featureFlagService.check(eq(FeatureFlagEnum.TENANT_TEST_FEATURE)))
.thenReturn(Mono.just(false));
CachedFeatures cachedFeatures = new CachedFeatures();
cachedFeatures.setFeatures(Map.of(FeatureFlagEnum.TENANT_TEST_FEATURE.name(), Boolean.FALSE));
Mockito.when(featureFlagService.getCachedTenantFeatureFlags()).thenReturn(cachedFeatures);
}
@Test
void testEEOnlyMethod() {
Mono<String> resultMono = testComponent.eeOnlyMethod();

View File

@ -1,62 +0,0 @@
package com.appsmith.server.featureflags.strategies;
import com.appsmith.server.domains.User;
import lombok.extern.slf4j.Slf4j;
import org.ff4j.core.FlippingExecutionContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Slf4j
public class EmailBasedRolloutStrategyTest {
EmailBasedRolloutStrategy strategy = new EmailBasedRolloutStrategy();
@BeforeEach
@WithUserDetails(value = "api_user")
public void setup() {
Map<String, String> initParams = new HashMap<>();
initParams.put("emailDomains", "example.com,another-example.com");
strategy.init("test-feature", initParams);
}
@Test
@WithUserDetails(value = "api_user")
public void testValidFeatureCheck() {
FlippingExecutionContext executionContext = Mockito.mock(FlippingExecutionContext.class);
User user = new User();
user.setEmail("test@EXAMPLE.com");
Mockito.when(executionContext.getValue(Mockito.anyString(), Mockito.anyBoolean()))
.thenReturn(user);
boolean evaluate = strategy.evaluate("test-feature", null, executionContext);
assertTrue(evaluate);
}
@Test
@WithUserDetails(value = "api_user")
public void testInvalidFeatureCheck() {
FlippingExecutionContext executionContext = Mockito.mock(FlippingExecutionContext.class);
User user = new User();
user.setEmail("test@random.com");
Mockito.when(executionContext.getValue(Mockito.anyString(), Mockito.anyBoolean()))
.thenReturn(user);
boolean evaluate = strategy.evaluate("test-feature", null, executionContext);
assertFalse(evaluate);
}
}

View File

@ -15,6 +15,11 @@ import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import static com.appsmith.server.featureflags.FeatureFlagEnum.TENANT_TEST_FEATURE;
import static com.appsmith.server.featureflags.FeatureFlagEnum.TEST_FEATURE_1;
import static com.appsmith.server.featureflags.FeatureFlagEnum.TEST_FEATURE_2;
@Profile("test")
@Primary
@ -26,7 +31,10 @@ public class MockCacheableFeatureFlagHelper implements CacheableFeatureFlagHelpe
public Mono<CachedFlags> fetchUserCachedFlags(String userIdentifier, User user) {
CachedFlags cachedFlags = new CachedFlags();
cachedFlags.setRefreshedAt(Instant.now());
cachedFlags.setFlags(new HashMap<>());
Map<String, Boolean> flags = new HashMap<>();
flags.put(TEST_FEATURE_1.name(), true);
flags.put(TEST_FEATURE_2.name(), false);
cachedFlags.setFlags(flags);
return Mono.just(cachedFlags);
}
@ -68,7 +76,9 @@ public class MockCacheableFeatureFlagHelper implements CacheableFeatureFlagHelpe
@Override
public Mono<FeaturesResponseDTO> getRemoteFeaturesForTenant(FeaturesRequestDTO featuresRequestDTO) {
FeaturesResponseDTO responseDTO = new FeaturesResponseDTO();
responseDTO.setFeatures(new HashMap<>());
Map<String, Boolean> features = new HashMap<>();
features.put(TENANT_TEST_FEATURE.name(), true);
responseDTO.setFeatures(features);
return Mono.just(responseDTO);
}
}

View File

@ -11,8 +11,6 @@ import com.appsmith.server.services.CacheableFeatureFlagHelper;
import com.appsmith.server.services.FeatureFlagService;
import com.appsmith.server.services.TenantService;
import lombok.extern.slf4j.Slf4j;
import org.ff4j.FF4j;
import org.ff4j.conf.XmlParser;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@ -21,10 +19,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.annotation.DirtiesContext;
@ -45,7 +41,6 @@ import static com.appsmith.server.featureflags.FeatureFlagEnum.TENANT_TEST_FEATU
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@ -93,30 +88,7 @@ public class FeatureFlagServiceCETest {
@WithUserDetails(value = "api_user")
public void testNullFeatureCheck() {
StepVerifier.create(featureFlagService.check(null))
.assertNext(result -> {
assertFalse(result);
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void testFeatureCheckForPonderationStrategy() {
Math.random();
StepVerifier.create(featureFlagService.check(FeatureFlagEnum.TEST_FEATURE_2))
.assertNext(result -> {
assertTrue(result);
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void testFeatureCheckForAppsmithUserStrategy() {
StepVerifier.create(featureFlagService.check(FeatureFlagEnum.TEST_FEATURE_1))
.assertNext(result -> {
assertFalse(result);
})
.assertNext(Assertions::assertFalse)
.verifyComplete();
}
@ -131,17 +103,6 @@ public class FeatureFlagServiceCETest {
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void testFeatureCheckForEmailStrategy() {
StepVerifier.create(featureFlagService.getAllFeatureFlagsForUser())
.assertNext(result -> {
assertNotNull(result);
assertTrue(result.containsKey(FeatureFlagEnum.TEST_FEATURE_3.toString()));
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void testGetFeaturesForUser_overrideWithTenantFeature() {
@ -150,12 +111,12 @@ public class FeatureFlagServiceCETest {
StepVerifier.create(featureFlagService.getAllFeatureFlagsForUser())
.assertNext(result -> {
assertNotNull(result);
assertNull(result.get(TENANT_TEST_FEATURE.toString()));
assertTrue(result.get(TENANT_TEST_FEATURE.toString()));
})
.verifyComplete();
Map<String, Boolean> tenantFeatures = new HashMap<>();
tenantFeatures.put(TENANT_TEST_FEATURE.toString(), true);
tenantFeatures.put(TENANT_TEST_FEATURE.toString(), false);
FeaturesResponseDTO responseDTO = new FeaturesResponseDTO();
responseDTO.setFeatures(tenantFeatures);
doReturn(Mono.just(responseDTO)).when(cacheableFeatureFlagHelper).getRemoteFeaturesForTenant(any());
@ -163,7 +124,7 @@ public class FeatureFlagServiceCETest {
StepVerifier.create(featureFlagService.getAllFeatureFlagsForUser())
.assertNext(result -> {
assertNotNull(result);
assertTrue(result.get(TENANT_TEST_FEATURE.toString()));
assertFalse(result.get(TENANT_TEST_FEATURE.toString()));
})
.verifyComplete();
}
@ -175,9 +136,7 @@ public class FeatureFlagServiceCETest {
Mono<CachedFlags> cachedFlagsMono = cacheableFeatureFlagHelper.fetchUserCachedFlags(userIdentifier, dummyUser);
Mono<Boolean> hasKeyMono = reactiveRedisTemplate.hasKey("featureFlag:" + userIdentifier);
StepVerifier.create(cachedFlagsMono.then(hasKeyMono))
.assertNext(isKeyPresent -> {
assertTrue(isKeyPresent);
})
.assertNext(Assertions::assertTrue)
.verifyComplete();
}
@ -187,9 +146,7 @@ public class FeatureFlagServiceCETest {
Mono<Void> evictCache = cacheableFeatureFlagHelper.evictUserCachedFlags(userIdentifier);
Mono<Boolean> hasKeyMono = reactiveRedisTemplate.hasKey("featureFlag:" + userIdentifier);
StepVerifier.create(evictCache.then(hasKeyMono))
.assertNext(isKeyPresent -> {
assertFalse(isKeyPresent);
})
.assertNext(Assertions::assertFalse)
.verifyComplete();
}
@ -324,34 +281,23 @@ public class FeatureFlagServiceCETest {
// Assert that the cached feature flags are empty before the remote fetch
CachedFeatures cachedFeaturesBeforeRemoteCall = featureFlagService.getCachedTenantFeatureFlags();
assertTrue(cachedFeaturesBeforeRemoteCall.getFeatures().isEmpty());
assertThat(cachedFeaturesBeforeRemoteCall.getFeatures().size()).isEqualTo(1);
assertTrue(cachedFeaturesBeforeRemoteCall.getFeatures().get(TENANT_TEST_FEATURE.name()));
Map<String, Boolean> tenantFeatures = new HashMap<>();
tenantFeatures.put(TENANT_TEST_FEATURE.name(), true);
tenantFeatures.put(TENANT_TEST_FEATURE.name(), false);
FeaturesResponseDTO responseDTO = new FeaturesResponseDTO();
responseDTO.setFeatures(tenantFeatures);
doReturn(Mono.just(responseDTO)).when(cacheableFeatureFlagHelper).getRemoteFeaturesForTenant(any());
StepVerifier.create(featureFlagService.getTenantFeatures())
.assertNext(result -> {
assertNotNull(result);
assertTrue(result.get(TENANT_TEST_FEATURE.name()));
assertFalse(result.get(TENANT_TEST_FEATURE.name()));
// Check if the cached feature flags are updated after the remote fetch
CachedFeatures cachedFeaturesAfterRemoteCall = featureFlagService.getCachedTenantFeatureFlags();
assertTrue(cachedFeaturesAfterRemoteCall.getFeatures().get(TENANT_TEST_FEATURE.name()));
assertFalse(cachedFeaturesAfterRemoteCall.getFeatures().get(TENANT_TEST_FEATURE.name()));
})
.verifyComplete();
}
@TestConfiguration
static class TestFeatureFlagConfig {
@Bean
FF4j ff4j() {
FF4j ff4j = new FF4j(new XmlParser(), "features/init-flags-test.xml")
.audit(true)
.autoCreate(false);
return ff4j;
}
}
}

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ff4j>
<autocreate>false</autocreate>
<audit>false</audit>
<features>
<feature uid="TEST_FEATURE_1" enable="true"
description="The test feature should only be visible to Appsmith users">
<flipstrategy class="com.appsmith.server.featureflags.strategies.AppsmithUserStrategy">
<param name="requiredKey" value="requiredValue"/>
</flipstrategy>
</feature>
<feature uid="TEST_FEATURE_2" enable="true" description="Enable this feature based on weight. It's randomized.">
<flipstrategy class="org.ff4j.strategy.PonderationStrategy">
<param name="weight" value="1"/>
</flipstrategy>
</feature>
<feature uid="TEST_FEATURE_3" enable="true"
description="The test feature should only be visible to certain users">
<flipstrategy class="com.appsmith.server.featureflags.strategies.EmailBasedRolloutStrategy">
<param name="emailDomains" value="test@example.com"/>
</flipstrategy>
</feature>
</features>
</ff4j>