diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java index fdad2fb8aada..3d26e6c8f3d0 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java @@ -29,6 +29,7 @@ public enum FeatureFlagEnum { release_git_autocommit_feature_enabled, release_git_autocommit_eligibility_enabled, release_dynamodb_connection_time_to_live_enabled, + release_reactive_actions_enabled, // Add EE flags below this line, to avoid conflicts. } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/RunBehaviourEnum.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/RunBehaviourEnum.java index f14f65791be0..e17bd29c1b90 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/RunBehaviourEnum.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/RunBehaviourEnum.java @@ -5,5 +5,6 @@ */ public enum RunBehaviourEnum { MANUAL, // Action will only run when manually triggered - ON_PAGE_LOAD // Action will run when the page loads + ON_PAGE_LOAD, // Action will run when the page loads + AUTOMATIC // Action will run automatically when a variable it depends on changes } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilCEImpl.java index 692056474797..89b6adbd4c66 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilCEImpl.java @@ -2,6 +2,7 @@ import com.appsmith.external.dtos.DslExecutableDTO; import com.appsmith.external.dtos.LayoutExecutableUpdateDTO; +import com.appsmith.external.enums.FeatureFlagEnum; import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.CreatorContextType; @@ -18,6 +19,7 @@ import com.appsmith.server.helpers.ObservationHelperImpl; import com.appsmith.server.onload.executables.ExecutableOnLoadService; import com.appsmith.server.services.AstService; +import com.appsmith.server.services.FeatureFlagService; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.observation.ObservationRegistry; @@ -90,6 +92,7 @@ public class OnLoadExecutablesUtilCEImpl implements OnLoadExecutablesUtilCE { private final Set APPSMITH_GLOBAL_VARIABLES = Set.of(); private final ObservationRegistry observationRegistry; private final ObservationHelperImpl observationHelper; + private final FeatureFlagService featureFlagService; /** * This function computes the sequenced on page load executables. @@ -302,120 +305,138 @@ public Mono updateExecutablesRunBehaviour( List executableUpdatesRef, List messagesRef, CreatorContextType creatorType) { - List toUpdateExecutables = new ArrayList<>(); - // Fetch all the actions which exist in this page. - Flux creatorContextExecutablesFlux = - this.getAllExecutablesByCreatorIdFlux(creatorId, creatorType).cache(); - - // Before we update the actions, fetch all the actions which are currently set to execute on load. - Mono> existingOnLoadExecutablesMono = creatorContextExecutablesFlux - .flatMap(executable -> { - if (RunBehaviourEnum.ON_PAGE_LOAD.equals(executable.getRunBehaviour())) { - return Mono.just(executable); - } - return Mono.empty(); - }) - .collectList(); + return featureFlagService + .check(FeatureFlagEnum.release_reactive_actions_enabled) + .flatMap(isReactiveActionsEnabled -> { + Flux creatorContextExecutablesFlux = this.getAllExecutablesByCreatorIdFlux( + creatorId, creatorType) + .cache(); + + return creatorContextExecutablesFlux.collectList().flatMap(creatorContextExecutables -> { + if (creatorContextExecutables.isEmpty()) { + messagesRef.clear(); + executableUpdatesRef.clear(); + return Mono.just(FALSE); + } - return existingOnLoadExecutablesMono - .zipWith(creatorContextExecutablesFlux.collectList()) - .flatMap(tuple -> { - List existingOnLoadExecutables = tuple.getT1(); - List creatorContextExecutables = tuple.getT2(); + // Determine existing "on" executables based on the flag, excluding userSetOnLoad=true + Set existingOnLoadExecutableNames = creatorContextExecutables.stream() + .filter(e -> { + if (Boolean.TRUE.equals(e.getUserSetOnLoad())) return false; + if (isReactiveActionsEnabled) { + return RunBehaviourEnum.ON_PAGE_LOAD.equals(e.getRunBehaviour()) + || RunBehaviourEnum.AUTOMATIC.equals(e.getRunBehaviour()); + } else { + return RunBehaviourEnum.ON_PAGE_LOAD.equals(e.getRunBehaviour()); + } + }) + .map(Executable::getUserExecutableName) + .collect(Collectors.toSet()); + + // Determine new "on" executables and their target RunBehaviour + // The `onLoadExecutables` parameter contains executables that should be "on". + // Their `runBehaviour` property should ideally already be set to the target state (AUTOMATIC or + // ON_PAGE_LOAD). + // If not explicitly set, we default based on the flag. + Map newOnLoadExecutablesWithTargetBehaviour = + onLoadExecutables.stream() + .collect(Collectors.toMap( + Executable::getUserExecutableName, + e -> e.getRunBehaviour() != null + ? e.getRunBehaviour() + : (isReactiveActionsEnabled + ? RunBehaviourEnum.AUTOMATIC + : RunBehaviourEnum.ON_PAGE_LOAD))); + Set newOnLoadExecutableNames = newOnLoadExecutablesWithTargetBehaviour.keySet(); + + Set turnedOffDueToLogic = new HashSet<>(existingOnLoadExecutableNames); + turnedOffDueToLogic.removeAll(newOnLoadExecutableNames); + + Set turnedOnDueToLogic = new HashSet<>(newOnLoadExecutableNames); + turnedOnDueToLogic.removeAll(existingOnLoadExecutableNames); + + List toUpdateExecutables = new ArrayList<>(); + + for (Executable executable : creatorContextExecutables) { + String executableName = executable.getUserExecutableName(); + // If a user has ever set execute on load, this field can not be changed automatically. + if (Boolean.TRUE.equals(executable.getUserSetOnLoad())) { + continue; // Skip updates for this executable + } - // There are no actions in this page. No need to proceed further since no actions would get updated - if (creatorContextExecutables.isEmpty()) { - return Mono.just(FALSE); - } + if (turnedOffDueToLogic.contains(executableName)) { + if (!RunBehaviourEnum.MANUAL.equals(executable.getRunBehaviour())) { + executable.setRunBehaviour(RunBehaviourEnum.MANUAL); + toUpdateExecutables.add(executable); + } + } else if (turnedOnDueToLogic.contains(executableName)) { + RunBehaviourEnum targetBehaviour = + newOnLoadExecutablesWithTargetBehaviour.get(executableName); + if (!targetBehaviour.equals(executable.getRunBehaviour())) { + executable.setRunBehaviour(targetBehaviour); + toUpdateExecutables.add(executable); + } + } + } - // No actions require an update if no actions have been found as page load actions as well as - // existing on load page actions are empty - if (existingOnLoadExecutables.isEmpty() - && (onLoadExecutables == null || onLoadExecutables.isEmpty())) { - return Mono.just(FALSE); - } + // Filter names for messages based on isOnLoadMessageAllowed + Set finalTurnedOffNamesForMessage = new HashSet<>(); + Set finalTurnedOnNamesForMessage = new HashSet<>(); - // Extract names of existing page load actions and new page load actions for quick lookup. - Set existingOnLoadExecutableNames = existingOnLoadExecutables.stream() - .map(Executable::getUserExecutableName) - .collect(Collectors.toSet()); - - Set newOnLoadExecutableNames = onLoadExecutables.stream() - .map(Executable::getUserExecutableName) - .collect(Collectors.toSet()); - - // Calculate the actions which would need to be updated from execute on load TRUE to FALSE. - Set turnedOffExecutableNames = new HashSet<>(); - turnedOffExecutableNames.addAll(existingOnLoadExecutableNames); - turnedOffExecutableNames.removeAll(newOnLoadExecutableNames); - - // Calculate the actions which would need to be updated from execute on load FALSE to TRUE - Set turnedOnExecutableNames = new HashSet<>(); - turnedOnExecutableNames.addAll(newOnLoadExecutableNames); - turnedOnExecutableNames.removeAll(existingOnLoadExecutableNames); - - for (Executable executable : creatorContextExecutables) { - - String executableName = executable.getUserExecutableName(); - // If a user has ever set execute on load, this field can not be changed automatically. It has - // to be explicitly changed by the user again. Add the executable to update only if this - // condition is false. - if (FALSE.equals(executable.getUserSetOnLoad())) { - - // If this executable is no longer an onload executable, turn the execute on load to false - if (turnedOffExecutableNames.contains(executableName)) { - executable.setRunBehaviour(RunBehaviourEnum.MANUAL); - toUpdateExecutables.add(executable); + for (Executable execInContext : creatorContextExecutables) { + String execName = execInContext.getUserExecutableName(); + if (Boolean.FALSE.equals(execInContext.isOnLoadMessageAllowed())) { + continue; } - - // If this executable is newly found to be on load, turn execute on load to true - if (turnedOnExecutableNames.contains(executableName)) { - executable.setRunBehaviour(RunBehaviourEnum.ON_PAGE_LOAD); - toUpdateExecutables.add(executable); + if (turnedOffDueToLogic.contains(execName) + && toUpdateExecutables.stream() + .anyMatch(u -> u.getId().equals(execInContext.getId()))) { + finalTurnedOffNamesForMessage.add(execName); } + if (turnedOnDueToLogic.contains(execName) + && toUpdateExecutables.stream() + .anyMatch(u -> u.getId().equals(execInContext.getId()))) { + finalTurnedOnNamesForMessage.add(execName); + } + } + messagesRef.clear(); + if (isReactiveActionsEnabled) { + if (!finalTurnedOffNamesForMessage.isEmpty()) { + messagesRef.add(finalTurnedOffNamesForMessage.toString() + + " will no longer run automatically. You can run it manually when needed."); + } + if (!finalTurnedOnNamesForMessage.isEmpty()) { + messagesRef.add( + finalTurnedOnNamesForMessage.toString() + + " will run automatically on page load or when a variable it depends on changes"); + } } else { - // Remove the executable name from either of the lists (if present) because this executable - // should not be updated - turnedOnExecutableNames.remove(executableName); - turnedOffExecutableNames.remove(executableName); + if (!finalTurnedOffNamesForMessage.isEmpty()) { + messagesRef.add(finalTurnedOffNamesForMessage.toString() + + " will no longer be executed on page load"); + } + if (!finalTurnedOnNamesForMessage.isEmpty()) { + messagesRef.add(finalTurnedOnNamesForMessage.toString() + + " will be executed automatically on page load"); + } } - } - - // Add newly turned on page actions to report back to the caller - executableUpdatesRef.addAll( - addExecutableUpdatesForExecutableNames(creatorContextExecutables, turnedOnExecutableNames)); - // Add newly turned off page actions to report back to the caller - executableUpdatesRef.addAll(addExecutableUpdatesForExecutableNames( - creatorContextExecutables, turnedOffExecutableNames)); + executableUpdatesRef.clear(); + executableUpdatesRef.addAll(toUpdateExecutables.stream() + .map(Executable::createLayoutExecutableUpdateDTO) + .collect(Collectors.toList())); - for (Executable executable : creatorContextExecutables) { - String executableName = executable.getUserExecutableName(); - if (Boolean.FALSE.equals(executable.isOnLoadMessageAllowed())) { - turnedOffExecutableNames.remove(executableName); - turnedOnExecutableNames.remove(executableName); + if (toUpdateExecutables.isEmpty()) { + return Mono.just(FALSE); + } else { + return Flux.fromIterable(toUpdateExecutables) + .flatMap(execToUpdate -> this.updateUnpublishedExecutable( + execToUpdate.getId(), execToUpdate, creatorType)) + .then(Mono.just(TRUE)); } - } - - // Now add messagesRef that would eventually be displayed to the developer user informing them - // about the action setting change. - if (!turnedOffExecutableNames.isEmpty()) { - messagesRef.add( - turnedOffExecutableNames.toString() + " will no longer be executed on page load"); - } - - if (!turnedOnExecutableNames.isEmpty()) { - messagesRef.add( - turnedOnExecutableNames.toString() + " will be executed automatically on page load"); - } - - // Finally update the actions which require an update - return Flux.fromIterable(toUpdateExecutables) - .flatMap(executable -> - this.updateUnpublishedExecutable(executable.getId(), executable, creatorType)) - .then(Mono.just(TRUE)); + }); }); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilImpl.java index acb26e326cbe..a6bd2f180652 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilImpl.java @@ -4,6 +4,7 @@ import com.appsmith.server.helpers.ObservationHelperImpl; import com.appsmith.server.onload.executables.ExecutableOnLoadService; import com.appsmith.server.services.AstService; +import com.appsmith.server.services.FeatureFlagService; import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.observation.ObservationRegistry; import lombok.extern.slf4j.Slf4j; @@ -18,7 +19,14 @@ public OnLoadExecutablesUtilImpl( ObjectMapper objectMapper, ExecutableOnLoadService pageExecutableOnLoadService, ObservationRegistry observationRegistry, - ObservationHelperImpl observationHelper) { - super(astService, objectMapper, pageExecutableOnLoadService, observationRegistry, observationHelper); + ObservationHelperImpl observationHelper, + FeatureFlagService featureFlagService) { + super( + astService, + objectMapper, + pageExecutableOnLoadService, + observationRegistry, + observationHelper, + featureFlagService); } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilCEImplTest.java new file mode 100644 index 000000000000..bc09b18d88ee --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/onload/internal/OnLoadExecutablesUtilCEImplTest.java @@ -0,0 +1,325 @@ +package com.appsmith.server.onload.internal; + +import com.appsmith.external.dtos.LayoutExecutableUpdateDTO; +import com.appsmith.external.models.ActionDTO; +import com.appsmith.external.models.CreatorContextType; +import com.appsmith.external.models.Executable; +import com.appsmith.external.models.RunBehaviourEnum; +import com.appsmith.server.helpers.ObservationHelperImpl; +import com.appsmith.server.onload.executables.ExecutableOnLoadService; +import com.appsmith.server.services.AstService; +import com.appsmith.server.services.FeatureFlagService; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +class OnLoadExecutablesUtilCEImplTest { + + @Mock + private AstService astService; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private ExecutableOnLoadService executableOnLoadService; + + @Mock + private FeatureFlagService featureFlagService; + + @Mock + private ObservationRegistry observationRegistry; + + @Mock + private ObservationHelperImpl observationHelper; + + @InjectMocks + private OnLoadExecutablesUtilCEImpl onLoadExecutablesUtilCEImpl; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + observationRegistry = ObservationRegistry.create(); // No-op registry + observationHelper = Mockito.mock(ObservationHelperImpl.class); + onLoadExecutablesUtilCEImpl = new OnLoadExecutablesUtilCEImpl( + astService, + objectMapper, + executableOnLoadService, + observationRegistry, + observationHelper, + featureFlagService); + } + + @Test + void whenNoExecutables_shouldReturnFalse() { + // Setup + List onLoadExecutables = new ArrayList<>(); + List executableUpdates = new ArrayList<>(); + List messages = new ArrayList<>(); + + when(executableOnLoadService.getAllExecutablesByCreatorIdFlux(any())).thenReturn(Flux.empty()); + when(featureFlagService.check(any())).thenReturn(Mono.just(true)); + + // Execute and verify + StepVerifier.create(onLoadExecutablesUtilCEImpl.updateExecutablesRunBehaviour( + onLoadExecutables, "creatorId", executableUpdates, messages, CreatorContextType.PAGE)) + .expectNext(false) + .verifyComplete(); + + // Assert + assert messages.isEmpty(); + assert executableUpdates.isEmpty(); + } + + @Test + void whenFeatureFlagOn_andExecutableTurnedOn_shouldShowReactiveMessage() { + // Setup + ActionDTO existingAction = new ActionDTO(); + existingAction.setName("TestApi"); + existingAction.setUserSetOnLoad(false); + existingAction.setRunBehaviour(RunBehaviourEnum.MANUAL); + existingAction.setId("1"); + + ActionDTO updatedAction = new ActionDTO(); + updatedAction.setName("TestApi"); + updatedAction.setUserSetOnLoad(false); + updatedAction.setRunBehaviour(RunBehaviourEnum.AUTOMATIC); + updatedAction.setId("1"); + + List onLoadExecutables = List.of(updatedAction); + List executableUpdates = new ArrayList<>(); + List messages = new ArrayList<>(); + + when(executableOnLoadService.getAllExecutablesByCreatorIdFlux(any())).thenReturn(Flux.just(existingAction)); + when(featureFlagService.check(any())).thenReturn(Mono.just(true)); + when(executableOnLoadService.updateUnpublishedExecutable(eq("1"), any())) + .thenReturn(Mono.just(updatedAction)); + + // Execute and verify + StepVerifier.create(onLoadExecutablesUtilCEImpl.updateExecutablesRunBehaviour( + onLoadExecutables, "creatorId", executableUpdates, messages, CreatorContextType.PAGE)) + .expectNext(true) + .verifyComplete(); + + // Assert + assert messages.size() == 1; + assert messages.get(0).contains("TestApi"); + assert messages.get(0).contains("will run automatically on page load or when a variable it depends on changes"); + } + + @Test + void whenFeatureFlagOff_andExecutableTurnedOn_shouldShowPageLoadMessage() { + // Setup + ActionDTO existingAction = new ActionDTO(); + existingAction.setName("TestApi"); + existingAction.setUserSetOnLoad(false); + existingAction.setRunBehaviour(RunBehaviourEnum.MANUAL); + existingAction.setId("1"); + + ActionDTO updatedAction = new ActionDTO(); + updatedAction.setName("TestApi"); + updatedAction.setUserSetOnLoad(false); + updatedAction.setRunBehaviour(RunBehaviourEnum.ON_PAGE_LOAD); + updatedAction.setId("1"); + + List onLoadExecutables = List.of(updatedAction); + List executableUpdates = new ArrayList<>(); + List messages = new ArrayList<>(); + + when(executableOnLoadService.getAllExecutablesByCreatorIdFlux(any())).thenReturn(Flux.just(existingAction)); + when(featureFlagService.check(any())).thenReturn(Mono.just(false)); + when(executableOnLoadService.updateUnpublishedExecutable(eq("1"), any())) + .thenReturn(Mono.just(updatedAction)); + + // Execute and verify + StepVerifier.create(onLoadExecutablesUtilCEImpl.updateExecutablesRunBehaviour( + onLoadExecutables, "creatorId", executableUpdates, messages, CreatorContextType.PAGE)) + .expectNext(true) + .verifyComplete(); + + // Assert + assert messages.size() == 1; + assert messages.get(0).contains("TestApi"); + assert messages.get(0).contains("will be executed automatically on page load"); + } + + @Test + void whenFeatureFlagOn_andExecutableTurnedOff_shouldShowManualMessage() { + // Setup + ActionDTO existingAction = new ActionDTO(); + existingAction.setName("TestApi"); + existingAction.setUserSetOnLoad(false); + existingAction.setRunBehaviour(RunBehaviourEnum.AUTOMATIC); + existingAction.setId("1"); + + ActionDTO updatedAction = new ActionDTO(); + updatedAction.setName("TestApi"); + updatedAction.setUserSetOnLoad(false); + updatedAction.setRunBehaviour(RunBehaviourEnum.MANUAL); + updatedAction.setId("1"); + + List onLoadExecutables = new ArrayList<>(); // Empty list means turning off + List executableUpdates = new ArrayList<>(); + List messages = new ArrayList<>(); + + when(executableOnLoadService.getAllExecutablesByCreatorIdFlux(any())).thenReturn(Flux.just(existingAction)); + when(featureFlagService.check(any())).thenReturn(Mono.just(true)); + when(executableOnLoadService.updateUnpublishedExecutable(eq("1"), any())) + .thenReturn(Mono.just(updatedAction)); + + // Execute and verify + StepVerifier.create(onLoadExecutablesUtilCEImpl.updateExecutablesRunBehaviour( + onLoadExecutables, "creatorId", executableUpdates, messages, CreatorContextType.PAGE)) + .expectNext(true) + .verifyComplete(); + + // Assert + assert messages.size() == 1; + assert messages.get(0).contains("TestApi"); + assert messages.get(0).contains("will no longer run automatically"); + } + + @Test + void whenMultipleExecutablesChange_shouldShowAllMessages() { + // Setup + ActionDTO existingAction1 = new ActionDTO(); + existingAction1.setName("Api1"); + existingAction1.setUserSetOnLoad(false); + existingAction1.setRunBehaviour(RunBehaviourEnum.AUTOMATIC); + existingAction1.setId("1"); + + ActionDTO existingAction2 = new ActionDTO(); + existingAction2.setName("Api2"); + existingAction2.setUserSetOnLoad(false); + existingAction2.setRunBehaviour(RunBehaviourEnum.MANUAL); + existingAction2.setId("2"); + + ActionDTO updatedAction1 = new ActionDTO(); + updatedAction1.setName("Api1"); + updatedAction1.setUserSetOnLoad(false); + updatedAction1.setRunBehaviour(RunBehaviourEnum.MANUAL); + updatedAction1.setId("1"); + + ActionDTO updatedAction2 = new ActionDTO(); + updatedAction2.setName("Api2"); + updatedAction2.setUserSetOnLoad(false); + updatedAction2.setRunBehaviour(RunBehaviourEnum.AUTOMATIC); + updatedAction2.setId("2"); + + List onLoadExecutables = List.of(updatedAction2); // Only Api2 is in the onLoad list + List executableUpdates = new ArrayList<>(); + List messages = new ArrayList<>(); + + when(executableOnLoadService.getAllExecutablesByCreatorIdFlux(any())) + .thenReturn(Flux.just(existingAction1, existingAction2)); + when(featureFlagService.check(any())).thenReturn(Mono.just(true)); + + // Mock different behaviors for different executable IDs + when(executableOnLoadService.updateUnpublishedExecutable(eq("1"), any())) + .thenReturn(Mono.just(updatedAction1)); + when(executableOnLoadService.updateUnpublishedExecutable(eq("2"), any())) + .thenReturn(Mono.just(updatedAction2)); + + // Execute and verify + StepVerifier.create(onLoadExecutablesUtilCEImpl.updateExecutablesRunBehaviour( + onLoadExecutables, "creatorId", executableUpdates, messages, CreatorContextType.PAGE)) + .expectNext(true) + .verifyComplete(); + + // Assert + assert messages.size() == 2; + assert messages.stream() + .anyMatch(msg -> msg.contains("Api1") && msg.contains("will no longer run automatically")); + assert messages.stream() + .anyMatch(msg -> msg.contains("Api2") + && msg.contains( + "will run automatically on page load or when a variable it depends on changes")); + } + + @Test + void whenNoStateChange_shouldReturnFalse() { + // Setup + ActionDTO existingAction = new ActionDTO(); + existingAction.setName("TestApi"); + existingAction.setUserSetOnLoad(false); + existingAction.setRunBehaviour(RunBehaviourEnum.AUTOMATIC); + existingAction.setId("1"); + + ActionDTO unchangedAction = new ActionDTO(); + unchangedAction.setName("TestApi"); + unchangedAction.setUserSetOnLoad(false); + unchangedAction.setRunBehaviour(RunBehaviourEnum.AUTOMATIC); + unchangedAction.setId("1"); + + List onLoadExecutables = List.of(unchangedAction); + List executableUpdates = new ArrayList<>(); + List messages = new ArrayList<>(); + + when(executableOnLoadService.getAllExecutablesByCreatorIdFlux(any())).thenReturn(Flux.just(existingAction)); + when(featureFlagService.check(any())).thenReturn(Mono.just(true)); + + // No actual call to updateUnpublishedExecutable should occur since no state changes + // Don't mock this method call as it shouldn't be invoked + + // Execute and verify + StepVerifier.create(onLoadExecutablesUtilCEImpl.updateExecutablesRunBehaviour( + onLoadExecutables, "creatorId", executableUpdates, messages, CreatorContextType.PAGE)) + .expectNext(false) + .verifyComplete(); + + // Assert + assert messages.isEmpty(); + assert executableUpdates.isEmpty(); + } + + @Test + void whenUserSetOnLoadIsTrue_shouldNotUpdateExecutable() { + // Setup + ActionDTO existingAction = new ActionDTO(); + existingAction.setName("TestApi"); + existingAction.setUserSetOnLoad(true); // User explicitly set the behavior + existingAction.setRunBehaviour(RunBehaviourEnum.MANUAL); + existingAction.setId("1"); + + ActionDTO updatedAction = new ActionDTO(); + updatedAction.setName("TestApi"); + updatedAction.setUserSetOnLoad(true); + updatedAction.setRunBehaviour(RunBehaviourEnum.AUTOMATIC); + updatedAction.setId("1"); + + List onLoadExecutables = List.of(updatedAction); + List executableUpdates = new ArrayList<>(); + List messages = new ArrayList<>(); + + when(executableOnLoadService.getAllExecutablesByCreatorIdFlux(any())).thenReturn(Flux.just(existingAction)); + when(featureFlagService.check(any())).thenReturn(Mono.just(true)); + + // No actual call to updateUnpublishedExecutable should occur since userSetOnLoad is true + // Don't mock this method call as it shouldn't be invoked + + // Execute and verify + StepVerifier.create(onLoadExecutablesUtilCEImpl.updateExecutablesRunBehaviour( + onLoadExecutables, "creatorId", executableUpdates, messages, CreatorContextType.PAGE)) + .expectNext(false) + .verifyComplete(); + + // Assert + assert messages.isEmpty(); + assert executableUpdates.isEmpty(); + } +}