diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/BrokerStepsParser.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/BrokerStepsParser.kt index 674715ec8cc5..c67e3bdaee82 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/BrokerStepsParser.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/BrokerStepsParser.kt @@ -29,12 +29,11 @@ import com.duckduckgo.pir.impl.store.PirRepository import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi -import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.withContext import logcat.LogPriority.ERROR import logcat.logcat import javax.inject.Inject +import javax.inject.Named interface BrokerStepsParser { /** @@ -97,28 +96,10 @@ interface BrokerStepsParser { class RealBrokerStepsParser @Inject constructor( private val dispatcherProvider: DispatcherProvider, private val repository: PirRepository, + @Named("pir") private val moshi: Moshi, ) : BrokerStepsParser { val adapter: JsonAdapter by lazy { - Moshi.Builder() - .add( - PolymorphicJsonAdapterFactory.of(BrokerAction::class.java, "actionType") - .withSubtype(BrokerAction.Extract::class.java, "extract") - .withSubtype(BrokerAction.Expectation::class.java, "expectation") - .withSubtype(BrokerAction.Click::class.java, "click") - .withSubtype(BrokerAction.FillForm::class.java, "fillForm") - .withSubtype(BrokerAction.Navigate::class.java, "navigate") - .withSubtype(BrokerAction.GetCaptchaInfo::class.java, "getCaptchaInfo") - .withSubtype(BrokerAction.SolveCaptcha::class.java, "solveCaptcha") - .withSubtype(BrokerAction.EmailConfirmation::class.java, "emailConfirmation"), - ) - .add( - PolymorphicJsonAdapterFactory.of(BrokerStep::class.java, "stepType") - .withSubtype(ScanStep::class.java, "scan") - .withSubtype(OptOutStep::class.java, "optOut"), - ) - .add(KotlinJsonAdapterFactory()) - .build() - .adapter(BrokerStep::class.java) + moshi.adapter(BrokerStep::class.java) } override suspend fun parseStep( diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/PirRunStateHandler.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/PirRunStateHandler.kt index 0f6698beb2fa..8ead44d0261c 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/PirRunStateHandler.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/PirRunStateHandler.kt @@ -38,13 +38,7 @@ import com.duckduckgo.pir.impl.models.ExtractedProfile import com.duckduckgo.pir.impl.pixels.PirPixelSender import com.duckduckgo.pir.impl.scheduling.JobRecordUpdater import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ClickResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExpectationResponse import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExtractedResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.FillFormResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.GetCaptchaInfoResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.NavigateResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.SolveCaptchaResponse import com.duckduckgo.pir.impl.store.PirEventsRepository import com.duckduckgo.pir.impl.store.PirRepository import com.duckduckgo.pir.impl.store.PirSchedulingRepository @@ -56,10 +50,9 @@ import com.duckduckgo.pir.impl.store.db.EmailConfirmationEventType.EMAIL_CONFIRM import com.duckduckgo.pir.impl.store.db.PirBrokerScanLog import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.Moshi -import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.withContext import javax.inject.Inject +import javax.inject.Named interface PirRunStateHandler { suspend fun handleState(pirRunState: PirRunState) @@ -170,24 +163,8 @@ class RealPirRunStateHandler @Inject constructor( private val jobRecordUpdater: JobRecordUpdater, private val pirSchedulingRepository: PirSchedulingRepository, private val currentTimeProvider: CurrentTimeProvider, + @Named("pir") private val moshi: Moshi, ) : PirRunStateHandler { - private val moshi: Moshi by lazy { - Moshi - .Builder() - .add( - PolymorphicJsonAdapterFactory - .of(PirSuccessResponse::class.java, "actionType") - .withSubtype(NavigateResponse::class.java, "navigate") - .withSubtype(ExtractedResponse::class.java, "extract") - .withSubtype(GetCaptchaInfoResponse::class.java, "getCaptchaInfo") - .withSubtype(SolveCaptchaResponse::class.java, "solveCaptcha") - .withSubtype(ClickResponse::class.java, "click") - .withSubtype(ExpectationResponse::class.java, "expectation") - .withSubtype(FillFormResponse::class.java, "fillForm"), - ).add(KotlinJsonAdapterFactory()) - .build() - } - private val pirSuccessAdapter by lazy { moshi.adapter(PirSuccessResponse::class.java) } override suspend fun handleState(pirRunState: PirRunState) = diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/ConditionExpectationSucceededEventHandler.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/ConditionExpectationSucceededEventHandler.kt new file mode 100644 index 000000000000..32d26c014841 --- /dev/null +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/ConditionExpectationSucceededEventHandler.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.pir.impl.common.actions + +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep +import com.duckduckgo.pir.impl.common.actions.EventHandler.Next +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.ConditionExpectationSucceeded +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.ExecuteBrokerStepAction +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.State +import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.UserProfile +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject +import kotlin.reflect.KClass + +@ContributesMultibinding( + scope = AppScope::class, + boundType = EventHandler::class, +) +class ConditionExpectationSucceededEventHandler @Inject constructor() : EventHandler { + override val event: KClass = ConditionExpectationSucceeded::class + + override suspend fun invoke( + state: State, + event: Event, + ): Next { + val actionsToAppend = (event as ConditionExpectationSucceeded).conditionActions + val currentBrokerStep = state.brokerStepsToExecute[state.currentBrokerStepIndex] + + val updatedBrokerSteps = state.brokerStepsToExecute.toMutableList() + val updatedBrokerActions = currentBrokerStep.actions.toMutableList().apply { + this.addAll( + state.currentActionIndex + 1, + actionsToAppend, + ) + } + val updatedBrokerStep = when (currentBrokerStep) { + is BrokerStep.ScanStep -> currentBrokerStep.copy(actions = updatedBrokerActions) + is BrokerStep.OptOutStep -> currentBrokerStep.copy(actions = updatedBrokerActions) + is BrokerStep.EmailConfirmationStep -> currentBrokerStep.copy(actions = updatedBrokerActions) + } + + updatedBrokerSteps[state.currentBrokerStepIndex] = updatedBrokerStep + + return Next( + nextState = state.copy( + currentActionIndex = state.currentActionIndex + 1, + brokerStepsToExecute = updatedBrokerSteps, + ), + nextEvent = ExecuteBrokerStepAction( + UserProfile( + userProfile = state.profileQuery, + ), + ), + ) + } +} diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/JsActionSuccessEventHandler.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/JsActionSuccessEventHandler.kt index a5679f2d25fc..7c9a8606988b 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/JsActionSuccessEventHandler.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/JsActionSuccessEventHandler.kt @@ -25,6 +25,7 @@ import com.duckduckgo.pir.impl.common.PirRunStateHandler.PirRunState.BrokerOptOu import com.duckduckgo.pir.impl.common.PirRunStateHandler.PirRunState.BrokerScanActionSucceeded import com.duckduckgo.pir.impl.common.actions.EventHandler.Next import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.ConditionExpectationSucceeded import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.ExecuteBrokerStepAction import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.JsActionSuccess import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.SideEffect.EvaluateJs @@ -33,6 +34,7 @@ import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.SideEf import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.State import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.UserProfile import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ClickResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ConditionResponse import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExpectationResponse import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExtractedResponse import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.FillFormResponse @@ -151,6 +153,28 @@ class JsActionSuccessEventHandler @Inject constructor( ), ) } + + is ConditionResponse -> { + if (pirSuccessResponse.response.actions.isNotEmpty()) { + Next( + nextState = baseSuccessState, + nextEvent = ConditionExpectationSucceeded( + pirSuccessResponse.response.actions, + ), + ) + } else { + Next( + nextState = baseSuccessState.copy( + currentActionIndex = baseSuccessState.currentActionIndex + 1, + ), + nextEvent = ExecuteBrokerStepAction( + UserProfile( + userProfile = baseSuccessState.profileQuery, + ), + ), + ) + } + } } } } diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/PirActionsRunnerStateEngine.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/PirActionsRunnerStateEngine.kt index aa413f9bd757..20c58983680d 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/PirActionsRunnerStateEngine.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/PirActionsRunnerStateEngine.kt @@ -122,6 +122,10 @@ interface PirActionsRunnerStateEngine { val actionId: String, val responseData: ResponseData?, ) : Event() + + data class ConditionExpectationSucceeded( + val conditionActions: List, + ) : Event() } /** diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/di/PirModule.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/di/PirModule.kt index 91f656d5f5b5..8e6e5392e425 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/di/PirModule.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/di/PirModule.kt @@ -24,6 +24,9 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.data.store.api.SharedPreferencesProvider import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.OptOutStep +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.ScanStep import com.duckduckgo.pir.impl.common.CaptchaResolver import com.duckduckgo.pir.impl.common.NativeBrokerActionHandler import com.duckduckgo.pir.impl.common.RealNativeBrokerActionHandler @@ -33,6 +36,19 @@ import com.duckduckgo.pir.impl.common.actions.RealPirActionsRunnerStateEngineFac import com.duckduckgo.pir.impl.scripts.BrokerActionProcessor import com.duckduckgo.pir.impl.scripts.PirMessagingInterface import com.duckduckgo.pir.impl.scripts.RealBrokerActionProcessor +import com.duckduckgo.pir.impl.scripts.models.BrokerAction +import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData +import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.SolveCaptcha +import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.UserProfile +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ClickResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ConditionResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExpectationResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExtractedResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.FillFormResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.GetCaptchaInfoResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.NavigateResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.SolveCaptchaResponse import com.duckduckgo.pir.impl.service.DbpService import com.duckduckgo.pir.impl.store.PirDatabase import com.duckduckgo.pir.impl.store.PirRepository @@ -48,10 +64,14 @@ import com.duckduckgo.pir.impl.store.db.ScanLogDao import com.duckduckgo.pir.impl.store.db.ScanResultsDao import com.duckduckgo.pir.impl.store.db.UserProfileDao import com.squareup.anvil.annotations.ContributesTo +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.Module import dagger.Provides import dagger.SingleInstanceIn import kotlinx.coroutines.CoroutineScope +import javax.inject.Named @Module @ContributesTo(AppScope::class) @@ -145,9 +165,10 @@ class PirModule { @Provides fun providesBrokerActionProcessor( pirMessagingInterface: PirMessagingInterface, + @Named("pir") moshi: Moshi, ): BrokerActionProcessor { // Creates a new instance everytime is BrokerActionProcessor injected - return RealBrokerActionProcessor(pirMessagingInterface) + return RealBrokerActionProcessor(pirMessagingInterface, moshi) } @Provides @@ -176,4 +197,43 @@ class PirModule { coroutineScope, ) } + + @Provides + @SingleInstanceIn(AppScope::class) + @Named("pir") + fun providePirMoshi(moshi: Moshi): Moshi { + return moshi.newBuilder() + .add( + PolymorphicJsonAdapterFactory.of(PirScriptRequestData::class.java, "data") + .withSubtype(SolveCaptcha::class.java, "solveCaptcha") + .withSubtype(UserProfile::class.java, "userProfile"), + ).add( + PolymorphicJsonAdapterFactory.of(BrokerAction::class.java, "actionType") + .withSubtype(BrokerAction.Extract::class.java, "extract") + .withSubtype(BrokerAction.Expectation::class.java, "expectation") + .withSubtype(BrokerAction.Click::class.java, "click") + .withSubtype(BrokerAction.FillForm::class.java, "fillForm") + .withSubtype(BrokerAction.Navigate::class.java, "navigate") + .withSubtype(BrokerAction.GetCaptchaInfo::class.java, "getCaptchaInfo") + .withSubtype(BrokerAction.SolveCaptcha::class.java, "solveCaptcha") + .withSubtype(BrokerAction.EmailConfirmation::class.java, "emailConfirmation") + .withSubtype(BrokerAction.Condition::class.java, "condition"), + ).add( + PolymorphicJsonAdapterFactory.of(BrokerStep::class.java, "stepType") + .withSubtype(ScanStep::class.java, "scan") + .withSubtype(OptOutStep::class.java, "optOut"), + ).add( + PolymorphicJsonAdapterFactory.of(PirSuccessResponse::class.java, "actionType") + .withSubtype(NavigateResponse::class.java, "navigate") + .withSubtype(ExtractedResponse::class.java, "extract") + .withSubtype(GetCaptchaInfoResponse::class.java, "getCaptchaInfo") + .withSubtype(SolveCaptchaResponse::class.java, "solveCaptcha") + .withSubtype(ClickResponse::class.java, "click") + .withSubtype(ExpectationResponse::class.java, "expectation") + .withSubtype(FillFormResponse::class.java, "fillForm") + .withSubtype(ConditionResponse::class.java, "condition"), + ) + .add(KotlinJsonAdapterFactory()) + .build() + } } diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/BrokerActionProcessor.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/BrokerActionProcessor.kt index 5e06b9c77659..b4a271546456 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/BrokerActionProcessor.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/BrokerActionProcessor.kt @@ -28,20 +28,9 @@ import com.duckduckgo.pir.impl.scripts.models.PirError import com.duckduckgo.pir.impl.scripts.models.PirResult import com.duckduckgo.pir.impl.scripts.models.PirScriptError import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData -import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.SolveCaptcha -import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.UserProfile import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestParams import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ClickResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExpectationResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExtractedResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.FillFormResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.GetCaptchaInfoResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.NavigateResponse -import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.SolveCaptchaResponse import com.squareup.moshi.Moshi -import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import logcat.LogPriority.ERROR import logcat.logcat import org.json.JSONObject @@ -74,43 +63,17 @@ interface BrokerActionProcessor { class RealBrokerActionProcessor( private val pirMessagingInterface: JsMessaging, + private val moshi: Moshi, ) : BrokerActionProcessor { private val requestAdapter by lazy { - Moshi.Builder() - .add( - PolymorphicJsonAdapterFactory.of(PirScriptRequestData::class.java, "data") - .withSubtype(SolveCaptcha::class.java, "solveCaptcha") - .withSubtype(UserProfile::class.java, "userProfile"), - ).add( - PolymorphicJsonAdapterFactory.of(BrokerAction::class.java, "actionType") - .withSubtype(BrokerAction.Extract::class.java, "extract") - .withSubtype(BrokerAction.Expectation::class.java, "expectation") - .withSubtype(BrokerAction.Click::class.java, "click") - .withSubtype(BrokerAction.FillForm::class.java, "fillForm") - .withSubtype(BrokerAction.Navigate::class.java, "navigate") - .withSubtype(BrokerAction.GetCaptchaInfo::class.java, "getCaptchaInfo") - .withSubtype(BrokerAction.SolveCaptcha::class.java, "solveCaptcha") - .withSubtype(BrokerAction.EmailConfirmation::class.java, "emailConfirmation"), - ).add(KotlinJsonAdapterFactory()) - .build() - .adapter(PirScriptRequestParams::class.java) + moshi.adapter(PirScriptRequestParams::class.java) } private val responseAdapter by lazy { - Moshi.Builder().add( - PolymorphicJsonAdapterFactory.of(PirSuccessResponse::class.java, "actionType") - .withSubtype(NavigateResponse::class.java, "navigate") - .withSubtype(ExtractedResponse::class.java, "extract") - .withSubtype(GetCaptchaInfoResponse::class.java, "getCaptchaInfo") - .withSubtype(SolveCaptchaResponse::class.java, "solveCaptcha") - .withSubtype(ClickResponse::class.java, "click") - .withSubtype(ExpectationResponse::class.java, "expectation") - .withSubtype(FillFormResponse::class.java, "fillForm"), - ).add(KotlinJsonAdapterFactory()) - .build().adapter(PirResult::class.java) + moshi.adapter(PirResult::class.java) } private val errorAdapter by lazy { - Moshi.Builder().build().adapter(PirScriptError::class.java) + moshi.adapter(PirScriptError::class.java) } private var registeredActionResultListener: ActionResultListener? = null diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/BrokerAction.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/BrokerAction.kt index 2fa32ba5ac5d..75f34e3c3a24 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/BrokerAction.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/BrokerAction.kt @@ -17,8 +17,10 @@ package com.duckduckgo.pir.impl.scripts.models import com.duckduckgo.pir.impl.scripts.models.BrokerAction.Click +import com.duckduckgo.pir.impl.scripts.models.BrokerAction.Condition import com.duckduckgo.pir.impl.scripts.models.BrokerAction.EmailConfirmation import com.duckduckgo.pir.impl.scripts.models.BrokerAction.Expectation +import com.duckduckgo.pir.impl.scripts.models.BrokerAction.Expectation.ExpectationSelector import com.duckduckgo.pir.impl.scripts.models.BrokerAction.Extract import com.duckduckgo.pir.impl.scripts.models.BrokerAction.FillForm import com.duckduckgo.pir.impl.scripts.models.BrokerAction.GetCaptchaInfo @@ -103,6 +105,14 @@ sealed class BrokerAction( override val id: String, val pollingTime: String, ) : BrokerAction(id) + + data class Condition( + override val id: String, + @Json(name = "_comment") + val comment: String, + val expectations: List, + val actions: List, + ) : BrokerAction(id) } data class ExtractProfileSelectors( @@ -169,5 +179,6 @@ fun BrokerAction.asActionType(): String { is GetCaptchaInfo -> "getCaptchaInfo" is SolveCaptcha -> "solveCaptcha" is EmailConfirmation -> "emailConfirmation" + is Condition -> "condition" } } diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/PirScriptResponseParams.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/PirScriptResponseParams.kt index 9fdb5ee0f57f..b9b2ec50c46a 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/PirScriptResponseParams.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/PirScriptResponseParams.kt @@ -127,4 +127,14 @@ sealed class PirSuccessResponse( data class AdditionalData( val additionalData: String, ) + + data class ConditionResponse( + override val actionID: String, + override val actionType: String, + val response: ResponseData = ResponseData(), + ) : PirSuccessResponse(actionID, actionType) { + data class ResponseData( + val actions: List = emptyList(), + ) + } } diff --git a/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/RealPirRunStateHandlerTest.kt b/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/RealPirRunStateHandlerTest.kt index e2539df769f9..6c6d653b4690 100644 --- a/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/RealPirRunStateHandlerTest.kt +++ b/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/RealPirRunStateHandlerTest.kt @@ -47,6 +47,7 @@ import com.duckduckgo.pir.impl.store.db.BrokerScanEventType.BROKER_SUCCESS import com.duckduckgo.pir.impl.store.db.EmailConfirmationEventType.EMAIL_CONFIRMATION_FAILED import com.duckduckgo.pir.impl.store.db.EmailConfirmationEventType.EMAIL_CONFIRMATION_SUCCESS import com.duckduckgo.pir.impl.store.db.PirBrokerScanLog +import com.squareup.moshi.Moshi import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -71,6 +72,7 @@ class RealPirRunStateHandlerTest { private val mockJobRecordUpdater: JobRecordUpdater = mock() private val mockSchedulingRepository: PirSchedulingRepository = mock() private val mockCurrentTimeProvider: CurrentTimeProvider = mock() + private val moshi: Moshi = Moshi.Builder().build() @Before fun setUp() { @@ -83,6 +85,7 @@ class RealPirRunStateHandlerTest { jobRecordUpdater = mockJobRecordUpdater, pirSchedulingRepository = mockSchedulingRepository, currentTimeProvider = mockCurrentTimeProvider, + moshi = moshi, ) whenever(mockCurrentTimeProvider.currentTimeMillis()).thenReturn(testEventTimeInMillis) diff --git a/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/ConditionExpectationSucceededEventHandlerTest.kt b/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/ConditionExpectationSucceededEventHandlerTest.kt new file mode 100644 index 000000000000..f5a612052fdb --- /dev/null +++ b/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/ConditionExpectationSucceededEventHandlerTest.kt @@ -0,0 +1,402 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.pir.impl.common.actions + +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.EmailConfirmationStep +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.OptOutStep +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.ScanStep +import com.duckduckgo.pir.impl.common.PirJob.RunType +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.ConditionExpectationSucceeded +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.ExecuteBrokerStepAction +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.State +import com.duckduckgo.pir.impl.models.ExtractedProfile +import com.duckduckgo.pir.impl.models.ProfileQuery +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord.EmailData +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord.JobAttemptData +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord.LinkFetchData +import com.duckduckgo.pir.impl.scripts.models.BrokerAction +import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.UserProfile +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class ConditionExpectationSucceededEventHandlerTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + + private lateinit var testee: ConditionExpectationSucceededEventHandler + + // Test data + private val testBrokerName = "test-broker" + private val testProfileQueryId = 123L + private val testCurrentActionIndex = 1 + + private val testProfileQuery = + ProfileQuery( + id = testProfileQueryId, + firstName = "John", + lastName = "Doe", + city = "New York", + state = "NY", + addresses = emptyList(), + birthYear = 1990, + fullName = "John Doe", + age = 33, + deprecated = false, + ) + + private val testExtractedProfile = + ExtractedProfile( + profileQueryId = testProfileQueryId, + brokerName = testBrokerName, + name = "John Doe", + ) + + private val testEmailConfirmationJob = + EmailConfirmationJobRecord( + brokerName = testBrokerName, + userProfileId = testProfileQueryId, + extractedProfileId = 456L, + emailData = EmailData( + email = "john@example.com", + attemptId = "test-attempt-id", + ), + linkFetchData = LinkFetchData( + emailConfirmationLink = "https://example.com/confirm", + linkFetchAttemptCount = 0, + lastLinkFetchDateInMillis = 0L, + ), + jobAttemptData = JobAttemptData( + jobAttemptCount = 0, + lastJobAttemptDateInMillis = 0L, + lastJobAttemptActionId = "", + ), + dateCreatedInMillis = 10000000L, + ) + + private val existingAction1 = + BrokerAction.Navigate( + id = "action-1", + url = "https://example.com", + ) + + private val existingAction2 = + BrokerAction.Click( + id = "action-2", + elements = emptyList(), + selector = null, + ) + + private val existingAction3 = + BrokerAction.Click( + id = "action-3", + elements = emptyList(), + selector = null, + ) + + private val conditionAction1 = + BrokerAction.FillForm( + id = "condition-action-1", + elements = emptyList(), + selector = "form-selector", + ) + + private val conditionAction2 = + BrokerAction.Click( + id = "condition-action-2", + elements = emptyList(), + selector = null, + ) + + @Before + fun setUp() { + testee = ConditionExpectationSucceededEventHandler() + } + + @Test + fun whenEventIsConditionExpectationSucceededThenEventTypeIsCorrect() { + assertEquals(ConditionExpectationSucceeded::class, testee.event) + } + + @Test + fun whenConditionActionsWithScanStepThenInsertsActionsAndReturnsNextEvent() = runTest { + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(existingAction1, existingAction2, existingAction3), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + ) + val conditionActions = listOf(conditionAction1, conditionAction2) + val event = ConditionExpectationSucceeded(conditionActions = conditionActions) + + val result = testee.invoke(state, event) + + // Verify actions are inserted at currentActionIndex + 1 (after action-2) + val updatedScanStep = result.nextState.brokerStepsToExecute[0] as ScanStep + val expectedActions = + listOf( + existingAction1, // action-1 + existingAction2, // action-2 (current action) + conditionAction1, // inserted action + conditionAction2, // inserted action + existingAction3, // action-3 + ) + assertEquals(expectedActions, updatedScanStep.actions) + + // Verify state is updated correctly + assertEquals(testCurrentActionIndex + 1, result.nextState.currentActionIndex) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + assertNull(result.sideEffect) + } + + @Test + fun whenConditionActionsWithOptOutStepThenInsertsActionsAndReturnsNextEvent() = runTest { + val optOutStep = + OptOutStep( + brokerName = testBrokerName, + stepType = "optout", + actions = listOf(existingAction1, existingAction2), + optOutType = "form", + profileToOptOut = testExtractedProfile, + ) + val state = + State( + runType = RunType.OPTOUT, + brokerStepsToExecute = listOf(optOutStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + ) + val conditionActions = listOf(conditionAction1) + val event = ConditionExpectationSucceeded(conditionActions = conditionActions) + + val result = testee.invoke(state, event) + + val updatedOptOutStep = result.nextState.brokerStepsToExecute[0] as OptOutStep + val expectedActions = + listOf( + existingAction1, + existingAction2, + conditionAction1, + ) + assertEquals(expectedActions, updatedOptOutStep.actions) + + assertEquals(testCurrentActionIndex + 1, result.nextState.currentActionIndex) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + } + + @Test + fun whenConditionActionsWithEmailConfirmationStepThenInsertsActionsAndReturnsNextEvent() = + runTest { + val emailConfirmationStep = + EmailConfirmationStep( + brokerName = testBrokerName, + stepType = "emailConfirmation", + actions = listOf(existingAction1, existingAction2), + emailConfirmationJob = testEmailConfirmationJob, + profileToOptOut = testExtractedProfile, + ) + val state = + State( + runType = RunType.EMAIL_CONFIRMATION, + brokerStepsToExecute = listOf(emailConfirmationStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + ) + val conditionActions = listOf(conditionAction1) + val event = ConditionExpectationSucceeded(conditionActions = conditionActions) + + val result = testee.invoke(state, event) + + val updatedEmailConfirmationStep = + result.nextState.brokerStepsToExecute[0] as EmailConfirmationStep + val expectedActions = + listOf( + existingAction1, + existingAction2, + conditionAction1, + ) + assertEquals(expectedActions, updatedEmailConfirmationStep.actions) + + assertEquals(testCurrentActionIndex + 1, result.nextState.currentActionIndex) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + } + + @Test + fun whenConditionActionsInsertedAtBeginningThenActionsAreInCorrectOrder() = runTest { + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(existingAction1, existingAction2), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = 0, // At the beginning + ) + val conditionActions = listOf(conditionAction1) + val event = ConditionExpectationSucceeded(conditionActions = conditionActions) + + val result = testee.invoke(state, event) + + val updatedScanStep = result.nextState.brokerStepsToExecute[0] as ScanStep + val expectedActions = + listOf( + existingAction1, + conditionAction1, // Inserted after index 0 + existingAction2, + ) + assertEquals(expectedActions, updatedScanStep.actions) + assertEquals(1, result.nextState.currentActionIndex) + } + + @Test + fun whenConditionActionsInsertedAtEndThenActionsAreAppended() = runTest { + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(existingAction1, existingAction2), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = 1, // At the last action + ) + val conditionActions = listOf(conditionAction1, conditionAction2) + val event = ConditionExpectationSucceeded(conditionActions = conditionActions) + + val result = testee.invoke(state, event) + + val updatedScanStep = result.nextState.brokerStepsToExecute[0] as ScanStep + val expectedActions = + listOf( + existingAction1, + existingAction2, + conditionAction1, // Inserted at end + conditionAction2, // Inserted at end + ) + assertEquals(expectedActions, updatedScanStep.actions) + assertEquals(2, result.nextState.currentActionIndex) + } + + @Test + fun whenMultipleBrokerStepsThenOnlyCurrentStepIsUpdated() = runTest { + val scanStep1 = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(existingAction1), + scanType = "initial", + ) + val scanStep2 = + ScanStep( + brokerName = "$testBrokerName-2", + stepType = "scan", + actions = listOf(existingAction2), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep1, scanStep2), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = 0, + ) + val conditionActions = listOf(conditionAction1) + val event = ConditionExpectationSucceeded(conditionActions = conditionActions) + + val result = testee.invoke(state, event) + + // Only the first step should be updated + val updatedScanStep1 = result.nextState.brokerStepsToExecute[0] as ScanStep + val unchangedScanStep2 = result.nextState.brokerStepsToExecute[1] as ScanStep + + assertEquals(2, updatedScanStep1.actions.size) + assertEquals(listOf(existingAction1, conditionAction1), updatedScanStep1.actions) + + assertEquals(1, unchangedScanStep2.actions.size) + assertEquals(listOf(existingAction2), unchangedScanStep2.actions) + } + + @Test + fun whenEmptyConditionActionsListThenOnlyActionIndexIsIncremented() = runTest { + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(existingAction1, existingAction2), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + ) + val event = ConditionExpectationSucceeded(conditionActions = emptyList()) + + val result = testee.invoke(state, event) + + // Actions should remain unchanged + val updatedScanStep = result.nextState.brokerStepsToExecute[0] as ScanStep + assertEquals(listOf(existingAction1, existingAction2), updatedScanStep.actions) + + // Action index should still increment + assertEquals(testCurrentActionIndex + 1, result.nextState.currentActionIndex) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + } +} diff --git a/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/JsActionSuccessEventHandlerTest.kt b/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/JsActionSuccessEventHandlerTest.kt new file mode 100644 index 000000000000..214f5ab93f47 --- /dev/null +++ b/pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/JsActionSuccessEventHandlerTest.kt @@ -0,0 +1,606 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.pir.impl.common.actions + +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.CurrentTimeProvider +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.EmailConfirmationStep +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.OptOutStep +import com.duckduckgo.pir.impl.common.BrokerStepsParser.BrokerStep.ScanStep +import com.duckduckgo.pir.impl.common.PirJob.RunType +import com.duckduckgo.pir.impl.common.PirRunStateHandler +import com.duckduckgo.pir.impl.common.PirRunStateHandler.PirRunState.BrokerOptOutActionSucceeded +import com.duckduckgo.pir.impl.common.PirRunStateHandler.PirRunState.BrokerScanActionSucceeded +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.ExecuteBrokerStepAction +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.Event.JsActionSuccess +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.SideEffect.EvaluateJs +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.SideEffect.GetCaptchaSolution +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.SideEffect.LoadUrl +import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.State +import com.duckduckgo.pir.impl.models.ExtractedProfile +import com.duckduckgo.pir.impl.models.ProfileQuery +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord.EmailData +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord.JobAttemptData +import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord.LinkFetchData +import com.duckduckgo.pir.impl.scripts.models.BrokerAction +import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.UserProfile +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ClickResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ConditionResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExpectationResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.ExtractedResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.FillFormResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.GetCaptchaInfoResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.NavigateResponse +import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.SolveCaptchaResponse +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class JsActionSuccessEventHandlerTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + + private lateinit var testee: JsActionSuccessEventHandler + + private val mockPirRunStateHandler: PirRunStateHandler = mock() + private val mockCurrentTimeProvider: CurrentTimeProvider = mock() + + // Test data + private val testBrokerName = "test-broker" + private val testProfileQueryId = 123L + private val testCurrentTimeInMillis = 2000L + private val testCurrentActionIndex = 1 + private val testActionRetryCount = 3 + + private val testProfileQuery = + ProfileQuery( + id = testProfileQueryId, + firstName = "John", + lastName = "Doe", + city = "New York", + state = "NY", + addresses = emptyList(), + birthYear = 1990, + fullName = "John Doe", + age = 33, + deprecated = false, + ) + + private val testExtractedProfile = + ExtractedProfile( + profileQueryId = testProfileQueryId, + brokerName = testBrokerName, + name = "John Doe", + ) + + private val testEmailConfirmationJob = + EmailConfirmationJobRecord( + brokerName = testBrokerName, + userProfileId = testProfileQueryId, + extractedProfileId = 456L, + emailData = EmailData( + email = "john@example.com", + attemptId = "test-attempt-id", + ), + linkFetchData = LinkFetchData( + emailConfirmationLink = "https://example.com/confirm", + linkFetchAttemptCount = 0, + lastLinkFetchDateInMillis = 0L, + ), + jobAttemptData = JobAttemptData( + jobAttemptCount = 0, + lastJobAttemptDateInMillis = 0L, + lastJobAttemptActionId = "", + ), + dateCreatedInMillis = 10000000L, + ) + + private val testAction = BrokerAction.Navigate( + id = "action-1", + url = "https://example.com", + ) + + @Before + fun setUp() { + testee = + JsActionSuccessEventHandler( + pirRunStateHandler = mockPirRunStateHandler, + currentTimeProvider = mockCurrentTimeProvider, + ) + + whenever(mockCurrentTimeProvider.currentTimeMillis()).thenReturn(testCurrentTimeInMillis) + } + + @Test + fun whenEventIsJsActionSuccessThenEventTypeIsCorrect() { + assertEquals(JsActionSuccess::class, testee.event) + } + + @Test + fun whenNavigateResponseWithScanStepThenReturnsLoadUrlSideEffect() = runTest { + val navigateResponse = + NavigateResponse( + actionID = "navigate-1", + actionType = "navigate", + response = NavigateResponse.ResponseData(url = "https://example.com/result"), + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = navigateResponse) + + val result = testee.invoke(state, event) + + val expectedState = + state.copy( + pendingUrl = "https://example.com/result", + actionRetryCount = 0, + ) + assertEquals(expectedState, result.nextState) + assertEquals(LoadUrl(url = "https://example.com/result"), result.sideEffect) + assertNull(result.nextEvent) + + val capturedState = argumentCaptor() + verify(mockPirRunStateHandler).handleState(capturedState.capture()) + assertEquals(testBrokerName, capturedState.firstValue.brokerName) + assertEquals(testProfileQueryId, capturedState.firstValue.profileQueryId) + assertEquals(navigateResponse, capturedState.firstValue.pirSuccessResponse) + } + + @Test + fun whenFillFormResponseWithScanStepThenReturnsNextActionEvent() = runTest { + val fillFormResponse = + FillFormResponse( + actionID = "fillform-1", + actionType = "fillForm", + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = fillFormResponse) + + val result = testee.invoke(state, event) + + val expectedState = + state.copy( + currentActionIndex = testCurrentActionIndex + 1, + actionRetryCount = 0, + ) + assertEquals(expectedState, result.nextState) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + assertNull(result.sideEffect) + + verify(mockPirRunStateHandler).handleState(any()) + } + + @Test + fun whenClickResponseWithScanStepThenReturnsNextActionEvent() = runTest { + val clickResponse = + ClickResponse( + actionID = "click-1", + actionType = "click", + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = clickResponse) + + val result = testee.invoke(state, event) + + val expectedState = + state.copy( + currentActionIndex = testCurrentActionIndex + 1, + actionRetryCount = 0, + ) + assertEquals(expectedState, result.nextState) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + assertNull(result.sideEffect) + } + + @Test + fun whenExpectationResponseWithScanStepThenReturnsNextActionEvent() = runTest { + val expectationResponse = + ExpectationResponse( + actionID = "expectation-1", + actionType = "expectation", + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = expectationResponse) + + val result = testee.invoke(state, event) + + val expectedState = + state.copy( + currentActionIndex = testCurrentActionIndex + 1, + actionRetryCount = 0, + ) + assertEquals(expectedState, result.nextState) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + assertNull(result.sideEffect) + } + + @Test + fun whenExtractedResponseWithScanStepThenReturnsNextActionEvent() = runTest { + val extractedResponse = + ExtractedResponse( + actionID = "extract-1", + actionType = "extract", + response = emptyList(), + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = extractedResponse) + + val result = testee.invoke(state, event) + + val expectedState = + state.copy( + currentActionIndex = testCurrentActionIndex + 1, + actionRetryCount = 0, + ) + assertEquals(expectedState, result.nextState) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + assertNull(result.sideEffect) + } + + @Test + fun whenGetCaptchaInfoResponseWithScanStepThenReturnsGetCaptchaSolutionSideEffect() = + runTest { + val captchaResponse = + GetCaptchaInfoResponse( + actionID = "captcha-info-1", + actionType = "getCaptchaInfo", + response = + GetCaptchaInfoResponse.ResponseData( + siteKey = "test-site-key", + url = "https://example.com", + type = "recaptcha", + ), + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = captchaResponse) + + val result = testee.invoke(state, event) + + val expectedState = state.copy(actionRetryCount = 0) + assertEquals(expectedState, result.nextState) + assertEquals( + GetCaptchaSolution( + actionId = "captcha-info-1", + responseData = captchaResponse.response, + isRetry = false, + ), + result.sideEffect, + ) + assertNull(result.nextEvent) + } + + @Test + fun whenSolveCaptchaResponseWithScanStepThenReturnsEvaluateJsSideEffectAndNextEvent() = + runTest { + val solveCaptchaResponse = + SolveCaptchaResponse( + actionID = "solve-captcha-1", + actionType = "solveCaptcha", + response = + SolveCaptchaResponse.ResponseData( + callback = SolveCaptchaResponse.CallbackData(eval = "callback-script"), + ), + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = solveCaptchaResponse) + + val result = testee.invoke(state, event) + + val expectedState = + state.copy( + currentActionIndex = testCurrentActionIndex + 1, + actionRetryCount = 0, + ) + assertEquals(expectedState, result.nextState) + assertEquals(EvaluateJs(callback = "callback-script"), result.sideEffect) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + } + + @Test + fun whenConditionResponseWithActionsThenReturnsConditionExpectationSucceededEvent() = + runTest { + val conditionActions = + listOf( + BrokerAction.Click( + id = "new-action-1", + elements = emptyList(), + selector = null, + ), + ) + val conditionResponse = + ConditionResponse( + actionID = "condition-1", + actionType = "condition", + response = ConditionResponse.ResponseData(actions = conditionActions), + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = conditionResponse) + + val result = testee.invoke(state, event) + + val expectedState = state.copy(actionRetryCount = 0) + assertEquals(expectedState, result.nextState) + assertEquals( + PirActionsRunnerStateEngine.Event.ConditionExpectationSucceeded(conditionActions), + result.nextEvent, + ) + assertNull(result.sideEffect) + } + + @Test + fun whenConditionResponseWithEmptyActionsThenReturnsNextActionEvent() = runTest { + val conditionResponse = + ConditionResponse( + actionID = "condition-1", + actionType = "condition", + response = ConditionResponse.ResponseData(actions = emptyList()), + ) + val scanStep = + ScanStep( + brokerName = testBrokerName, + stepType = "scan", + actions = listOf(testAction), + scanType = "initial", + ) + val state = + State( + runType = RunType.MANUAL, + brokerStepsToExecute = listOf(scanStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = conditionResponse) + + val result = testee.invoke(state, event) + + val expectedState = + state.copy( + currentActionIndex = testCurrentActionIndex + 1, + actionRetryCount = 0, + ) + assertEquals(expectedState, result.nextState) + assertEquals( + ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery)), + result.nextEvent, + ) + } + + @Test + fun whenOptOutStepThenHandlesBrokerOptOutActionSucceeded() = runTest { + val navigateResponse = + NavigateResponse( + actionID = "navigate-1", + actionType = "navigate", + response = NavigateResponse.ResponseData(url = "https://example.com/result"), + ) + val optOutStep = + OptOutStep( + brokerName = testBrokerName, + stepType = "optout", + actions = listOf(testAction), + optOutType = "form", + profileToOptOut = testExtractedProfile, + ) + val state = + State( + runType = RunType.OPTOUT, + brokerStepsToExecute = listOf(optOutStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = navigateResponse) + + val result = testee.invoke(state, event) + + val capturedState = argumentCaptor() + verify(mockPirRunStateHandler).handleState(capturedState.capture()) + assertEquals(testBrokerName, capturedState.firstValue.brokerName) + assertEquals(testExtractedProfile, capturedState.firstValue.extractedProfile) + assertEquals(testCurrentTimeInMillis, capturedState.firstValue.completionTimeInMillis) + assertEquals("navigate", capturedState.firstValue.actionType) + assertEquals(navigateResponse, capturedState.firstValue.result) + + assertEquals(LoadUrl(url = "https://example.com/result"), result.sideEffect) + } + + @Test + fun whenEmailConfirmationStepThenHandlesBrokerOptOutActionSucceeded() = runTest { + val navigateResponse = + NavigateResponse( + actionID = "navigate-1", + actionType = "navigate", + response = NavigateResponse.ResponseData(url = "https://example.com/result"), + ) + val emailConfirmationStep = + EmailConfirmationStep( + brokerName = testBrokerName, + stepType = "emailConfirmation", + actions = listOf(testAction), + emailConfirmationJob = testEmailConfirmationJob, + profileToOptOut = testExtractedProfile, + ) + val state = + State( + runType = RunType.EMAIL_CONFIRMATION, + brokerStepsToExecute = listOf(emailConfirmationStep), + profileQuery = testProfileQuery, + currentBrokerStepIndex = 0, + currentActionIndex = testCurrentActionIndex, + actionRetryCount = testActionRetryCount, + ) + val event = JsActionSuccess(pirSuccessResponse = navigateResponse) + + val result = testee.invoke(state, event) + + val capturedState = argumentCaptor() + verify(mockPirRunStateHandler).handleState(capturedState.capture()) + assertEquals(testBrokerName, capturedState.firstValue.brokerName) + assertEquals(testExtractedProfile, capturedState.firstValue.extractedProfile) + assertEquals(testCurrentTimeInMillis, capturedState.firstValue.completionTimeInMillis) + assertEquals("navigate", capturedState.firstValue.actionType) + assertEquals(navigateResponse, capturedState.firstValue.result) + + assertEquals(LoadUrl(url = "https://example.com/result"), result.sideEffect) + } +}