From 8dfca5cf28e1a334a54dcb25aa5c2265b6caea12 Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 11:05:02 +0200 Subject: [PATCH 01/33] chore(build): exclude Kotlin files from Javadoc generation and decrease minSdk to 24 SUITEDEV-35510 Co-authored-by: Andras Sarro --- build.gradle.kts | 5 ++++- gradle/libs.versions.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 107766cb..a99df0a5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,8 +83,11 @@ tasks { } } } -} + withType(Javadoc::class.java).all { + exclude("**/*.kt") + } +} nexusPublishing { packageGroup = "com.emarsys" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ea4190b..63176e5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ androidx-navigation-compose = "2.7.6" compose-compiler = "1.5.6" compose-plugin = "1.6.0-dev1419" android-compileSdk = "34" -android-minSdk = "26" +android-minSdk = "24" android-targetSdk = "34" androidx-activity = "1.8.2" androidx-activityCompose = "1.8.2" From 0c90e731df89fb8c6ff14f01636c5735e9aff2c6 Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 11:57:45 +0200 Subject: [PATCH 02/33] chore(build): exclude Kotlin files from Javadoc generation for sample app SUITEDEV-35510 Co-authored-by: Andras Sarro --- sample/build.gradle.kts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index cece039d..5b96e492 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -56,7 +56,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get().toString() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } if (env.fetch("RELEASE_MODE", (System.getenv("RELEASE_MODE") ?: "false")) == "true") { @@ -153,4 +153,10 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) coreLibraryDesugaring(libs.android.tools.desugar) +} + +tasks { + withType(Javadoc::class.java).all { + exclude("**/*.kt") + } } \ No newline at end of file From 3e3f9979036a527582cf1d66579ec2684a43b82e Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 13:42:59 +0200 Subject: [PATCH 03/33] chore(build): exclude Kotlin files from Javadoc generation for all modules SUITEDEV-35510 Co-authored-by: Andras Sarro --- build.gradle.kts | 5 ++++- sample/build.gradle.kts | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a99df0a5..ebf796a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,8 +83,11 @@ tasks { } } } +} - withType(Javadoc::class.java).all { +allprojects { + // Exclude Kotlin files from Javadoc generation because Kotlin files are not supported by Dokka + tasks.withType(Javadoc::class.java).all { exclude("**/*.kt") } } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 5b96e492..f7df7cfe 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -154,9 +154,3 @@ dependencies { coreLibraryDesugaring(libs.android.tools.desugar) } - -tasks { - withType(Javadoc::class.java).all { - exclude("**/*.kt") - } -} \ No newline at end of file From 329f5920de886839db532a3afb00aa5cf1ca0c6c Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 14:04:47 +0200 Subject: [PATCH 04/33] chore(build): disable Javadoc generation for all modules SUITEDEV-35510 Co-authored-by: Andras Sarro --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ebf796a1..ee905b94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -87,8 +87,8 @@ tasks { allprojects { // Exclude Kotlin files from Javadoc generation because Kotlin files are not supported by Dokka - tasks.withType(Javadoc::class.java).all { - exclude("**/*.kt") + tasks.withType(Javadoc::class).all { + enabled = false } } From 8d67dd2b4568b01534a1cd6a6eb46e4bd5ccb28b Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 15:22:00 +0200 Subject: [PATCH 05/33] chore(build): refactor on_tag_workflow SUITEDEV-35510 Co-authored-by: Andras Sarro --- .github/workflows/on_tag_workflow.yml | 133 +++++--------------------- 1 file changed, 24 insertions(+), 109 deletions(-) diff --git a/.github/workflows/on_tag_workflow.yml b/.github/workflows/on_tag_workflow.yml index 9cf573e8..9d9f6aa2 100644 --- a/.github/workflows/on_tag_workflow.yml +++ b/.github/workflows/on_tag_workflow.yml @@ -6,6 +6,23 @@ on: - '*.*.*' env: + USE_LOCAL_DEPENDENCY: ${{ vars.USE_LOCAL_DEPENDENCY }} + RELEASE_MODE: true + ANDROID_RELEASE_STORE_FILE_BASE64: ${{ secrets.ANDROID_RELEASE_STORE_FILE_BASE64 }} + ANDROID_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }} + ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} + ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} + FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} + FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} + GOOGLE_OAUTH_SERVER_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_SERVER_CLIENT_ID }} + GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} + GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64 }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SONATYPE_SIGNING_KEY_ID: ${{ secrets.SONATYPE_SIGNING_KEY_ID }} + SONATYPE_SIGNING_PASSWORD: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} + SONATYPE_SIGNING_SECRET_KEY_RING_FILE_BASE64: ${{ secrets.SONATYPE_SIGNING_SECRET_KEY_RING_FILE_BASE64 }} RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD }} RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }} RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} @@ -17,9 +34,6 @@ env: SLACK_USERNAME: Emarsys SDK - Android EXCLUDE_GOOGLE_SERVICES_API_KEY: true SIGNING_SECRET_KEY_RING_FILE: /home/runner/work/android-emarsys-sdk/android-emarsys-sdk/secring.asc.gpg - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} SIGNING_KEYID: ${{ secrets.SIGNING_KEYID }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} @@ -38,120 +52,20 @@ jobs: with: distribution: 'temurin' java-version: 17 - - name: create-google services json - uses: jsdaniell/create-json@1.1.2 - with: - name: google-services.json - json: ${{ secrets.GOOGLE_SERVICES_JSON }} - dir: sample - name: setup RELEASE_VERSION run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Create release keystore file - shell: bash - run: | - echo "${{ secrets.ANDROID_KEYSTORE }}" > mobile-team-android.jks.asc - gpg -d --passphrase "${{ secrets.ANDROID_GPG_PASSWORD }}" --batch mobile-team-android.jks.asc > sample/mobile-team-android.jks - - - name: Create mavenCentral keystore file - shell: bash - run: | - echo "${{ secrets.SIGNING_KEY_FILE_ASC }}" > secring.asc - gpg --dearmor secring.asc > secring.gpg - - - name: create local.properties - run: echo $ANDROID_HOME > local.properties + - name: Prepare CI + run: make prepare-ci - name: Release with Gradle - run: ./gradlew clean assembleRelease && ./gradlew publishToSonatype - - CreateSampleReleaseBundle: - name: Create release bundle from sample app - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: true - fetch-depth: 0 # 0 indicates all history - - run: git fetch --all || echo "==> Accept any result" - - name: set up JDK 1.17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: 17 - - name: create-google services json - uses: jsdaniell/create-json@1.1.2 - with: - name: google-services.json - json: ${{ secrets.GOOGLE_SERVICES_JSON }} - dir: sample - - - name: create local.properties - run: echo $ANDROID_HOME > local.properties - - - name: setup RELEASE_VERSION - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - - name: Create release keystore file - shell: bash - run: | - echo "${{ secrets.ANDROID_KEYSTORE }}" > mobile-team-android.jks.asc - gpg -d --passphrase "${{ secrets.ANDROID_GPG_PASSWORD }}" --batch mobile-team-android.jks.asc > sample/mobile-team-android.jks - - name: Create release sample app with Gradle - run: ./gradlew :sample:bundleRelease - - - name: Upload bundle - uses: actions/upload-artifact@v4 - with: - name: mobile-sdk-sample - path: sample/build/outputs/bundle/release/sample-release.aab - - ReleaseSample: - name: Release sample app - runs-on: ubuntu-latest - needs: [Release, CreateSampleReleaseBundle] - steps: - - uses: actions/checkout@v3 - with: - submodules: true - fetch-depth: 0 # 0 indicates all history - - run: git fetch --all || echo "==> Accept any result" - - - name: Download sample app - uses: actions/download-artifact@v4 - with: - name: mobile-sdk-sample - - - name: create-google services json - uses: jsdaniell/create-json@1.1.2 - with: - name: google-play-services.json - json: ${{ secrets.GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON }} - - - name: Playstore upload - uses: r0adkll/upload-google-play@v1.1.3 - with: - serviceAccountJson: google-play-services.json - packageName: com.emarsys.sample - releaseFiles: sample-release.aab - - name: setup RELEASE_VERSION - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Create Release Page - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ env.RELEASE_VERSION }} - release_name: ${{ github.ref }} - body_path: changelog.md - draft: false - prerelease: false + run: make release SlackNotification: name: Send slack notification runs-on: ubuntu-latest - needs: [ReleaseSample] + needs: [Release] steps: - name: setup RELEASE_VERSION run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV @@ -188,10 +102,11 @@ jobs: job_status: 'success' user_icon: '${{env.SLACK_ICON}}' actions: '[{ "type": "button", "text": "View actions", "url": "https://github.com/emartech/android-emarsys-sdk/actions" },{ "type": "button", "text": "View Firebase", "url": "https://console.firebase.google.com/project/ems-mobile-sdk/testlab/histories/" },{ "type": "button", "text": "Install page", "url": "http://ems-mobileteam-artifacts.s3-website-eu-west-1.amazonaws.com/index-ems.html" }]' + CreateJiraTicket: name: Create Jira ticket runs-on: ubuntu-latest - needs: [ReleaseSample] + needs: [Release] steps: - name: Login to Jira uses: atlassian/gajira-login@master @@ -215,7 +130,7 @@ jobs: OnError: name: Handle on Error runs-on: ubuntu-latest - needs: [ReleaseSample] + needs: [Release] if: ${{ failure() }} steps: - uses: actions/checkout@v3 From 71cc2ebba380f161c7af692077170680c5706a1d Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 15:38:31 +0200 Subject: [PATCH 06/33] chore(build): refactor on_tag_workflow SUITEDEV-35510 Co-authored-by: megamegax Co-authored-by: Andras Sarro --- .github/workflows/on_tag_workflow.yml | 78 +++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/.github/workflows/on_tag_workflow.yml b/.github/workflows/on_tag_workflow.yml index 9d9f6aa2..d354337c 100644 --- a/.github/workflows/on_tag_workflow.yml +++ b/.github/workflows/on_tag_workflow.yml @@ -62,10 +62,82 @@ jobs: - name: Release with Gradle run: make release + CreateSampleReleaseBundle: + name: Create release bundle from sample app + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 # 0 indicates all history + - run: git fetch --all || echo "==> Accept any result" + - name: set up JDK 1.17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + + - name: Prepare CI + run: make prepare-ci + + - name: Prepare sample release + run: make prepare-sample-release + + - name: Create sample app release bundle + run: make create-sample-release-bundle + + - name: Upload bundle + uses: actions/upload-artifact@v4 + with: + name: mobile-sdk-sample + path: sample/build/outputs/bundle/release/sample-release.aab + + ReleaseSample: + name: Release sample app + runs-on: ubuntu-latest + needs: [Release, CreateSampleReleaseBundle] + steps: + - uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 # 0 indicates all history + - run: git fetch --all || echo "==> Accept any result" + + - name: Download sample app + uses: actions/download-artifact@v4 + with: + name: mobile-sdk-sample + + - name: create-google services json + uses: jsdaniell/create-json@1.1.2 + with: + name: google-play-services.json + json: ${{ secrets.GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON }} + + - name: Playstore upload + uses: r0adkll/upload-google-play@v1.1.3 + with: + serviceAccountJson: google-play-services.json + packageName: com.emarsys.sample + releaseFiles: sample-release.aab + + - name: setup RELEASE_VERSION + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Create Release Page + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.RELEASE_VERSION }} + release_name: ${{ github.ref }} + body_path: changelog.md + draft: false + prerelease: false + SlackNotification: name: Send slack notification runs-on: ubuntu-latest - needs: [Release] + needs: [ReleaseSample] steps: - name: setup RELEASE_VERSION run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV @@ -106,7 +178,7 @@ jobs: CreateJiraTicket: name: Create Jira ticket runs-on: ubuntu-latest - needs: [Release] + needs: [ReleaseSample] steps: - name: Login to Jira uses: atlassian/gajira-login@master @@ -130,7 +202,7 @@ jobs: OnError: name: Handle on Error runs-on: ubuntu-latest - needs: [Release] + needs: [ReleaseSample] if: ${{ failure() }} steps: - uses: actions/checkout@v3 From 030940f95c20f9def076bca151402b36b5c6e35d Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 15:53:03 +0200 Subject: [PATCH 07/33] fix(release): fix sample app release upload SUITEDEV-35510 Co-authored-by: megamegax Co-authored-by: Andras Sarro --- .github/workflows/on_tag_workflow.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/on_tag_workflow.yml b/.github/workflows/on_tag_workflow.yml index d354337c..4429f0b5 100644 --- a/.github/workflows/on_tag_workflow.yml +++ b/.github/workflows/on_tag_workflow.yml @@ -108,18 +108,15 @@ jobs: with: name: mobile-sdk-sample - - name: create-google services json - uses: jsdaniell/create-json@1.1.2 - with: - name: google-play-services.json - json: ${{ secrets.GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON }} + - name: Prepare sample release + run: make prepare-sample-release - - name: Playstore upload + - name: PlayStore upload uses: r0adkll/upload-google-play@v1.1.3 with: - serviceAccountJson: google-play-services.json + serviceAccountJson: ./sample/google-play-store-service-account.json packageName: com.emarsys.sample - releaseFiles: sample-release.aab + releaseFiles: sample/build/outputs/bundle/release/sample-release.aab - name: setup RELEASE_VERSION run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV From ddda00352a32b7faa18f3f6a0c4e2be1633cccf6 Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 16:10:49 +0200 Subject: [PATCH 08/33] fix(release): fix sample app release upload SUITEDEV-35510 Co-authored-by: megamegax Co-authored-by: Andras Sarro --- .github/workflows/on_tag_workflow.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/on_tag_workflow.yml b/.github/workflows/on_tag_workflow.yml index 4429f0b5..386360cf 100644 --- a/.github/workflows/on_tag_workflow.yml +++ b/.github/workflows/on_tag_workflow.yml @@ -103,6 +103,12 @@ jobs: fetch-depth: 0 # 0 indicates all history - run: git fetch --all || echo "==> Accept any result" + - name: set up JDK 1.17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + - name: Download sample app uses: actions/download-artifact@v4 with: From ee6643aa5dfb933cf8cb764c89154f6ad50c2605 Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Mon, 8 Apr 2024 16:22:51 +0200 Subject: [PATCH 09/33] fix(release): fix sample app release upload SUITEDEV-35510 Co-authored-by: megamegax Co-authored-by: Andras Sarro --- .github/workflows/on_tag_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on_tag_workflow.yml b/.github/workflows/on_tag_workflow.yml index 386360cf..4dcd0f1f 100644 --- a/.github/workflows/on_tag_workflow.yml +++ b/.github/workflows/on_tag_workflow.yml @@ -122,7 +122,7 @@ jobs: with: serviceAccountJson: ./sample/google-play-store-service-account.json packageName: com.emarsys.sample - releaseFiles: sample/build/outputs/bundle/release/sample-release.aab + releaseFiles: sample-release.aab - name: setup RELEASE_VERSION run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV From 94d4224af2f28c8bfc6f6d3f5c0f47191aeebb96 Mon Sep 17 00:00:00 2001 From: megamegax Date: Thu, 2 May 2024 14:06:19 +0200 Subject: [PATCH 10/33] fix(in-app): catch all kinds of exception for further investigation SUITEDEV-35723 Co-authored-by: kovacszsoltizsolt <22084766+kovacszsoltizsolt@users.noreply.github.com> Co-authored-by: LordAndras <49073629+LordAndras@users.noreply.github.com> --- .../java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt index 24c7fa5c..e67c52fa 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt @@ -11,7 +11,6 @@ import com.emarsys.core.util.log.entry.InAppLoadingTime import com.emarsys.mobileengage.iam.dialog.IamDialog import com.emarsys.mobileengage.iam.dialog.IamDialogProvider import com.emarsys.mobileengage.iam.model.InAppMetaData -import com.emarsys.mobileengage.iam.webview.IamWebViewCreationFailedException import com.emarsys.mobileengage.iam.webview.MessageLoadedListener @Mockable @@ -56,7 +55,7 @@ class OverlayInAppPresenter( showingInProgress = false } } - } catch (e: IamWebViewCreationFailedException) { + } catch (e: Exception) { concurrentHandlerHolder.coreHandler.post { Logger.error(CrashLog(e)) messageLoadedListener?.onMessageLoaded() From 3808812da1d7a79e582f889c0f9723f33fd82988 Mon Sep 17 00:00:00 2001 From: LordAndras Date: Tue, 7 May 2024 11:16:25 +0200 Subject: [PATCH 11/33] fix(push-payload): map 'u' and 'message_id' to notificationData and pass them to the eventHandler SUITEDEV-35736 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- .../InappNotificationIntegrationTest.kt | 4 +- .../NotificationCommandFactoryTest.kt | 13 ++- .../command/DismissNotificationCommandTest.kt | 4 +- .../PreloadedInappHandlerCommandTest.kt | 4 +- .../mobileengage/service/IntentUtilsTest.kt | 4 +- .../service/MessagingServiceUtilsTest.kt | 88 +++++++++++++---- .../service/NotificationActionUtilsTest.kt | 4 +- .../service/RemoteMessageMapperV1Test.kt | 88 ++++++++++++----- .../service/RemoteMessageMapperV2Test.kt | 99 ++++++++++++++----- .../NotificationCommandFactory.kt | 2 + .../mobileengage/service/NotificationData.kt | 4 +- .../service/mapper/RemoteMessageMapperV1.kt | 14 ++- .../service/mapper/RemoteMessageMapperV2.kt | 13 ++- 13 files changed, 261 insertions(+), 80 deletions(-) diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt index 711fd9cb..a8d0422c 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt @@ -129,7 +129,9 @@ class InappNotificationIntegrationTest : AnnotationSpec() { operation = NotificationOperation.INIT.name, actions = null, defaultAction = null, - inapp = inappPayload + inapp = inappPayload, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val intent = IntentUtils.createNotificationHandlerServiceIntent( application, diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/NotificationCommandFactoryTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/NotificationCommandFactoryTest.kt index 4bd98c80..836fdbaf 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/NotificationCommandFactoryTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/NotificationCommandFactoryTest.kt @@ -54,6 +54,8 @@ class NotificationCommandFactoryTest : AnnotationSpec() { const val MULTICHANNEL_ID = "test multiChannel id" const val SMALL_RESOURCE_ID = 123 const val COLOR_RESOURCE_ID = 456 + const val MESSAGE_ID = "messageId" + const val U = "{\"customField\":\"customValue\"}" val notificationMethod = NotificationMethod(COLLAPSE_ID, NotificationOperation.INIT) val notificationData = NotificationData( null, @@ -74,12 +76,12 @@ class NotificationCommandFactoryTest : AnnotationSpec() { rootParams = mapOf( "rootParamKey1" to "rootParamValue1", "rootParamKey2" to "rootParamValue2" - ) + ), + u = U, + message_id = MESSAGE_ID ) } - - private lateinit var factory: NotificationCommandFactory private lateinit var context: Context private lateinit var mockConcurrentHandlerHolder: ConcurrentHandlerHolder @@ -425,6 +427,7 @@ class NotificationCommandFactoryTest : AnnotationSpec() { mapOf( "rootParamKey1" to "rootParamValue1", "rootParamKey2" to "rootParamValue2", + "u" to U, "title" to TITLE, "body" to BODY, "channelId" to CHANNEL_ID, @@ -446,7 +449,9 @@ class NotificationCommandFactoryTest : AnnotationSpec() { ) ) ) - ) + ), + "u" to U, + "message_id" to MESSAGE_ID ) ) JsonUtils.toFlatMap(payload!!) shouldBe JsonUtils.toFlatMap(expected) diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/DismissNotificationCommandTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/DismissNotificationCommandTest.kt index 6cf51b52..d45226f5 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/DismissNotificationCommandTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/DismissNotificationCommandTest.kt @@ -38,7 +38,9 @@ class DismissNotificationCommandTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) } diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/PreloadedInappHandlerCommandTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/PreloadedInappHandlerCommandTest.kt index f00b3087..b0faf821 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/PreloadedInappHandlerCommandTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/PreloadedInappHandlerCommandTest.kt @@ -50,7 +50,9 @@ class PreloadedInappHandlerCommandTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = NotificationOperation.INIT.name, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) } diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/IntentUtilsTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/IntentUtilsTest.kt index 9f1abdc4..5cd9355e 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/IntentUtilsTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/IntentUtilsTest.kt @@ -40,7 +40,9 @@ class IntentUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = NotificationOperation.INIT.name, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) } diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt index a70fbcd1..514ad459 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt @@ -77,7 +77,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) } @@ -221,6 +223,8 @@ class MessagingServiceUtilsTest : AnnotationSpec() { operation = OPERATION, actions = null, inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val ems = JSONObject() @@ -289,7 +293,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) MessagingServiceUtils.createNotification( @@ -317,6 +323,8 @@ class MessagingServiceUtilsTest : AnnotationSpec() { operation = OPERATION, actions = null, inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val input: MutableMap = HashMap() @@ -353,7 +361,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val input: MutableMap = HashMap() @@ -389,7 +399,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val result = MessagingServiceUtils.createNotification( @@ -420,7 +432,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val result = MessagingServiceUtils.createNotification( @@ -454,7 +468,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val result = MessagingServiceUtils.createNotification( @@ -488,7 +504,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val expectedColor = ContextCompat.getColor(context, colorResourceId) @@ -520,7 +538,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val result = MessagingServiceUtils.createNotification( @@ -550,7 +570,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val result = MessagingServiceUtils.createNotification( @@ -580,7 +602,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val notificationSettings: NotificationSettings = mock() @@ -617,7 +641,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val notificationSettings: NotificationSettings = mock() @@ -672,7 +698,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = actions.toString(), - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val result = MessagingServiceUtils.createNotification( @@ -715,7 +743,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val notificationSettings: NotificationSettings = mock() @@ -750,7 +780,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) val notificationSettings: NotificationSettings = mock() @@ -872,7 +904,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) MessagingServiceUtils.createSilentPushCommands( @@ -897,7 +931,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) MessagingServiceUtils.createSilentPushCommands( @@ -958,7 +994,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = actions, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) MessagingServiceUtils.createSilentPushCommands( @@ -1017,7 +1055,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = actions, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) MessagingServiceUtils.createSilentPushCommands( @@ -1062,7 +1102,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ), mockFileDownloader, deviceInfo @@ -1095,7 +1137,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ), mockFileDownloader, deviceInfo @@ -1132,7 +1176,9 @@ class MessagingServiceUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ), mockFileDownloader, deviceInfo diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/NotificationActionUtilsTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/NotificationActionUtilsTest.kt index a2a346cf..e81c6599 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/NotificationActionUtilsTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/NotificationActionUtilsTest.kt @@ -36,7 +36,9 @@ class NotificationActionUtilsTest : AnnotationSpec() { collapseId = COLLAPSE_ID, operation = OPERATION, actions = null, - inapp = null + inapp = null, + u = "{\"customField\":\"customValue\"}", + message_id = "messageId" ) } diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV1Test.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV1Test.kt index 47f53668..1cee0bbe 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV1Test.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV1Test.kt @@ -10,6 +10,8 @@ import com.emarsys.mobileengage.di.setupMobileEngageComponent import com.emarsys.mobileengage.di.tearDownMobileEngageComponent import com.emarsys.mobileengage.fake.FakeMobileEngageDependencyContainer import com.emarsys.mobileengage.service.mapper.RemoteMessageMapperV1 +import com.emarsys.mobileengage.service.mapper.RemoteMessageMapperV1.Companion.MISSING_MESSAGE_ID +import com.emarsys.mobileengage.service.mapper.RemoteMessageMapperV1.Companion.MISSING_SID import com.emarsys.testUtil.AnnotationSpec import com.emarsys.testUtil.InstrumentationRegistry import io.kotest.matchers.shouldBe @@ -78,7 +80,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { val ems = JSONObject() ems.put("style", "THUMBNAIL") - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["channel_id"] = CHANNEL_ID input["ems"] = ems.toString() @@ -94,7 +96,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_whenTitleIsMissing() { - val input: MutableMap = createRemoteMessage(title = null) + val input: MutableMap = createRemoteMessage(title = null, u = "{}") input["channel_id"] = CHANNEL_ID val notificationData = remoteMessageMapperV1.map(input) @@ -115,7 +117,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_whenImageIsAvailable() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["image_url"] = IMAGE_URL input["channel_id"] = CHANNEL_ID @@ -127,7 +129,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_whenImageIsNotAvailable() { val testImageUrl = "https://fa.il/img.jpg" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["image_url"] = testImageUrl input["channel_id"] = CHANNEL_ID @@ -140,7 +142,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { fun testMap_whenNotificationMethodIsSet() { val collapseId = "testNotificationId" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["image_url"] = IMAGE_URL input["channel_id"] = CHANNEL_ID val notificationMethodJson = JSONObject() @@ -158,7 +160,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_whenNotificationMethodIsMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["image_url"] = IMAGE_URL input["channel_id"] = CHANNEL_ID val ems = JSONObject() @@ -171,7 +173,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_whenNotificationMethodIsSet_withoutCollapseID_shouldReturnWithInitOperation() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["image_url"] = IMAGE_URL input["channel_id"] = CHANNEL_ID val ems = JSONObject() @@ -187,7 +189,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_whenNotificationMethodIsSet_withCollapseID_shouldReturnWithSetOperation() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["image_url"] = IMAGE_URL input["channel_id"] = CHANNEL_ID val ems = JSONObject() @@ -204,7 +206,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_whenNotificationMethodIsSet_withCollapseID_shouldReturnWithInitOperation_whenOperationIsMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") input["image_url"] = IMAGE_URL input["channel_id"] = CHANNEL_ID val ems = JSONObject() @@ -221,7 +223,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_campaignId() { val testCampaignId = "test campaign id" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") val ems = JSONObject() ems.put("multichannelId", testCampaignId) input["ems"] = ems.toString() @@ -233,17 +235,26 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_sid() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = """{"sid":"$SID"}""") val notificationData = remoteMessageMapperV1.map(input) notificationData.sid shouldBe SID } + @Test + fun testMap_notificationData_shouldContain_missingSid() { + val input: MutableMap = createRemoteMessage(u = """{}""") + + val notificationData = remoteMessageMapperV1.map(input) + + notificationData.sid shouldBe MISSING_SID + } + @Test fun testMap_notificationData_shouldContain_actions() { val testActions = "test actions" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") val ems = JSONObject() ems.put("actions", testActions) input["ems"] = ems.toString() @@ -255,7 +266,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_null_ifActionsAreMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") val notificationData = remoteMessageMapperV1.map(input) @@ -265,7 +276,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_defaultAction() { val testDefaultAction = "test default action" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") val ems = JSONObject() ems.put("default_action", testDefaultAction) input["ems"] = ems.toString() @@ -277,7 +288,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_null_ifDefaultActionIsMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") val ems = JSONObject() input["ems"] = ems.toString() val notificationData = remoteMessageMapperV1.map(input) @@ -288,7 +299,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_inapp() { val testInapp = "test inapp" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") val ems = JSONObject() ems.put("inapp", testInapp) input["ems"] = ems.toString() @@ -300,7 +311,7 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_null_ifInappIsMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage(u = "{}") val notificationData = remoteMessageMapperV1.map(input) @@ -313,25 +324,58 @@ class RemoteMessageMapperV1Test : AnnotationSpec() { "key1" to "value1", "key2" to "value2", ) - val input: MutableMap = createRemoteMessage(rootParam = rootParam) + val input: MutableMap = createRemoteMessage( + rootParam = rootParam, + u = "{}" + ) val notificationData = remoteMessageMapperV1.map(input) notificationData.rootParams shouldBe rootParam } + @Test + fun testMap_notificationData_should_map_u_param() { + val u = """{"sid":"$SID", "key1":"value1"}""" + + val input: MutableMap = createRemoteMessage(u = u) + + val notificationData = remoteMessageMapperV1.map(input) + + notificationData.u shouldBe u + } + + @Test + fun testMap_notificationData_should_map_message_id() { + val testMessageId = "testMessageId" + val input: MutableMap = createRemoteMessage(u = "{}") + input["message_id"] = testMessageId + + val notificationData = remoteMessageMapperV1.map(input) + + notificationData.message_id shouldBe testMessageId + } + + @Test + fun testMap_notificationData_should_contain_MissingMessageId() { + val input: MutableMap = createRemoteMessage(u = "{}") + + val notificationData = remoteMessageMapperV1.map(input) + + notificationData.message_id shouldBe MISSING_MESSAGE_ID + } + private fun createRemoteMessage( title: String? = TITLE, body: String = BODY, - sid: String = SID, - rootParam: Map = mapOf() + rootParam: Map = mapOf(), + u: String? = null ): MutableMap { val payload = mutableMapOf() title?.let { payload["title"] = it } payload["body"] = body - val uObject = """{"sid":"$sid"}""" - payload["u"] = uObject + payload["u"] = u return (payload + rootParam).toMutableMap() } diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV2Test.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV2Test.kt index b23c9bd2..48c41e07 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV2Test.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/RemoteMessageMapperV2Test.kt @@ -10,6 +10,8 @@ import com.emarsys.mobileengage.di.setupMobileEngageComponent import com.emarsys.mobileengage.di.tearDownMobileEngageComponent import com.emarsys.mobileengage.fake.FakeMobileEngageDependencyContainer import com.emarsys.mobileengage.service.mapper.RemoteMessageMapperV2 +import com.emarsys.mobileengage.service.mapper.RemoteMessageMapperV2.Companion.MISSING_MESSAGE_ID +import com.emarsys.mobileengage.service.mapper.RemoteMessageMapperV2.Companion.MISSING_SID import com.emarsys.testUtil.AnnotationSpec import com.emarsys.testUtil.InstrumentationRegistry import io.kotest.matchers.shouldBe @@ -83,7 +85,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { ) .thenReturn(COLOR_RESOURCE_ID) - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["notification.channel_id"] = CHANNEL_ID input["ems.style"] = "THUMBNAIL" @@ -99,7 +101,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_whenTitleIsMissing() { - val input: MutableMap = createRemoteMessage(title = null) + val input: MutableMap = createRemoteMessage(title = null) input["notification.channel_id"] = CHANNEL_ID val notificationData = remoteMessageMapperV2.map(input) @@ -111,7 +113,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_whenMapIsEmpty() { - val input: MutableMap = HashMap() + val input: MutableMap = HashMap() val notificationData = remoteMessageMapperV2.map(input) @@ -120,7 +122,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_whenImageIsAvailable() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["notification.image"] = IMAGE_URL val notificationData = remoteMessageMapperV2.map(input) @@ -131,7 +133,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_whenImageIsNotAvailable() { val testImageUrl = "https://fa.il/img.jpg" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["notification.image"] = testImageUrl val notificationData = remoteMessageMapperV2.map(input) @@ -142,7 +144,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_whenNotificationMethodIsSet() { val collapseId = "testNotificationId" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.notification_method.collapse_key"] = collapseId input["ems.notification_method.operation"] = "UPDATE" @@ -154,7 +156,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_whenNotificationMethodIsMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() val notificationData = remoteMessageMapperV2.map(input) @@ -163,7 +165,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_whenNotificationMethodIsSet_withoutCollapseID_shouldReturnWithInitOperation() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.notification_method.operation"] = "UPDATE" val notificationData = remoteMessageMapperV2.map(input) @@ -174,7 +176,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_campaignId() { val testCampaignId = "test campaign id" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.multichannel_id"] = testCampaignId val notificationData = remoteMessageMapperV2.map(input) @@ -184,13 +186,22 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_sid() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() val notificationData = remoteMessageMapperV2.map(input) notificationData.sid shouldBe SID } + @Test + fun testMap_notificationData_shouldContain_sidDefaultValue() { + val input: MutableMap = createRemoteMessage(sid = null) + + val notificationData = remoteMessageMapperV2.map(input) + + notificationData.sid shouldBe MISSING_SID + } + @Test fun testMap_notificationData_shouldContain_actions() { val testActions = JSONArray().put( @@ -206,7 +217,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { val expectedActions = """[{"type":"MECustomEvent","id":"Testing","title":{"en":"Test title"},"name":"test action name"}]""" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.actions"] = testActions.toString() val notificationData = remoteMessageMapperV2.map(input) @@ -216,7 +227,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_null_ifDefaultActionTypeIsMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.tap_actions.default_action.name"] = "test name" input["ems.tap_actions.default_action.url"] = "test url" input["ems.tap_actions.default_action.payload"] = """{"key":"test payload"}""" @@ -228,7 +239,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_defaultAction() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.tap_actions.default_action.name"] = "test name" input["ems.tap_actions.default_action.type"] = "MECustomEvent" input["ems.tap_actions.default_action.url"] = "test url" @@ -244,7 +255,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_null_ifActionsAreMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() val notificationData = remoteMessageMapperV2.map(input) @@ -254,7 +265,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_inapp() { val testInapp = "test inapp" - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.inapp"] = testInapp val notificationData = remoteMessageMapperV2.map(input) @@ -264,7 +275,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_notificationData_shouldContain_null_ifInappIsMissing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() val notificationData = remoteMessageMapperV2.map(input) @@ -273,7 +284,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_rootParams_shouldContain_rootParams_as_map() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.root_params"] = """{"key1":"value1","key2":"123"}""" val expectedRootParams = mapOf( @@ -288,7 +299,7 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { @Test fun testMap_rootParams_shouldContain_rootParams_as_map_even_if_the_value_is_a_json() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() input["ems.root_params"] = """{"key1":"value1","key2":"123","key3":{"test":"test"}}""" val expectedRootParams = mapOf( @@ -302,9 +313,53 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { notificationData.rootParams shouldBe expectedRootParams } + @Test + fun testMap_u_shouldContain_correctValue() { + val input: MutableMap = createRemoteMessage() + input["ems.root_params"] = """{"key1":"value1","key2":"123","key3":{"test":"test"}, "u":{"customField":"customValue"}}""" + + val expectedU = """{"customField":"customValue"}""" + + val notificationData = remoteMessageMapperV2.map(input) + + notificationData.u shouldBe expectedU + } + + @Test + fun testMap_u_shouldContain_emptyJsonAsString() { + val input: MutableMap = createRemoteMessage() + input["ems.root_params"] = """{"key1":"value1","key2":"123","key3":{"test":"test"}, "u":{}}""" + + val expectedU = "{}" + + val notificationData = remoteMessageMapperV2.map(input) + + notificationData.u shouldBe expectedU + } + + @Test + fun testMap_messageId_shouldContain_correctValue() { + val testMessageId = "testMessageId" + val input: MutableMap = createRemoteMessage() + input["ems.message_id"] = testMessageId + + val notificationData = remoteMessageMapperV2.map(input) + + notificationData.message_id shouldBe testMessageId + } + + @Test + fun testMap_messageId_shouldContain_defaultValue() { + val input: MutableMap = createRemoteMessage() + + val notificationData = remoteMessageMapperV2.map(input) + + notificationData.message_id shouldBe MISSING_MESSAGE_ID + } + @Test fun testMap_rootParams_shouldContain_empty_map_if_rootParams_are_missing() { - val input: MutableMap = createRemoteMessage() + val input: MutableMap = createRemoteMessage() val expectedRootParams = mapOf() @@ -317,9 +372,9 @@ class RemoteMessageMapperV2Test : AnnotationSpec() { private fun createRemoteMessage( title: String? = TITLE, body: String = BODY, - sid: String = SID - ): MutableMap { - val payload = mutableMapOf() + sid: String? = SID + ): MutableMap { + val payload = mutableMapOf() title?.let { payload["notification.title"] = it } payload["notification.body"] = body payload["ems.sid"] = sid diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/notification/NotificationCommandFactory.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/notification/NotificationCommandFactory.kt index e7ef75b8..e984c11b 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/notification/NotificationCommandFactory.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/notification/NotificationCommandFactory.kt @@ -180,6 +180,8 @@ class NotificationCommandFactory(private val context: Context) { json.put("actions", actions) json.put("defaultAction", notificationData.defaultAction) json.put("inapp", notificationData.inapp) + json.put("u", notificationData.u) + json.put("message_id", notificationData.message_id) return json } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationData.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationData.kt index 5601a85a..ad329257 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationData.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationData.kt @@ -20,7 +20,9 @@ data class NotificationData( val actions: String? = null, val defaultAction: String? = null, val inapp: String? = null, - val rootParams: Map = mapOf() + val rootParams: Map = mapOf(), + val u: String, + val message_id: String ) : Parcelable enum class NotificationOperation { diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV1.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV1.kt index 6888db00..b6341001 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV1.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV1.kt @@ -20,11 +20,17 @@ class RemoteMessageMapperV1( private val context: Context, private val uuidProvider: UUIDProvider ) : RemoteMessageMapper { + companion object { + const val MISSING_SID = "Missing sid" + const val MISSING_MESSAGE_ID = "Missing messageId" + const val EMPTY_U = "{}" + } override fun map(remoteMessageData: Map): NotificationData { val resourceIds = getNotificationResourceIds() val messageDataCopy = remoteMessageData.toMutableMap() + val messageId = messageDataCopy.remove("message_id") ?: MISSING_MESSAGE_ID val image = messageDataCopy.remove("image_url") val iconImage = messageDataCopy.remove("icon_url") val title = messageDataCopy.remove("title") @@ -33,8 +39,8 @@ class RemoteMessageMapperV1( val campaignId = ems.optString("multichannelId") val body = messageDataCopy.remove("body") val channelId = messageDataCopy.remove("channel_id") - val sid = messageDataCopy.remove("u")?.let { JSONObject(it).getNullableString("sid") } - ?: "Missing sid" + val u = messageDataCopy.remove("u") ?: EMPTY_U + val sid = JSONObject(u).getNullableString("sid") ?: MISSING_SID val notificationMethod: NotificationMethod = if (ems.has("notificationMethod")) { parseNotificationMethod(ems.optJSONObject("notificationMethod")) } else { @@ -60,7 +66,9 @@ class RemoteMessageMapperV1( actions = actions, defaultAction = defaultAction, inapp = inapp, - rootParams = messageDataCopy + rootParams = messageDataCopy, + u = u, + message_id = messageId ) } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV2.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV2.kt index 7eabcbc7..89be28fa 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV2.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/mapper/RemoteMessageMapperV2.kt @@ -17,9 +17,15 @@ class RemoteMessageMapperV2( private val context: Context, private val uuidProvider: UUIDProvider ) : RemoteMessageMapper { + companion object { + const val MISSING_SID = "Missing sid" + const val MISSING_MESSAGE_ID = "Missing messageId" + const val EMPTY_U = "{}" + } override fun map(remoteMessageData: Map): NotificationData { val resourceIds = getNotificationResourceIds() + val messageId = remoteMessageData["ems.message_id"] ?: MISSING_MESSAGE_ID val image = remoteMessageData["notification.image"] val iconImage = remoteMessageData["notification.icon"] val title = remoteMessageData["notification.title"] @@ -28,11 +34,12 @@ class RemoteMessageMapperV2( val body = remoteMessageData["notification.body"] val channelId = remoteMessageData["notification.channel_id"] val notificationMethod: NotificationMethod = parseNotificationMethod(remoteMessageData) - val sid = remoteMessageData["ems.sid"] ?: "Missing sid" + val sid = remoteMessageData["ems.sid"] ?: MISSING_SID val actions = remoteMessageData["ems.actions"] val defaultAction = extractDefaultAction(remoteMessageData) val inapp = remoteMessageData["ems.inapp"] val rootParams = extractRootParams(remoteMessageData["ems.root_params"]) + val u = rootParams["u"] ?: EMPTY_U return NotificationData( imageUrl = image, @@ -50,7 +57,9 @@ class RemoteMessageMapperV2( actions = actions, defaultAction = defaultAction, inapp = inapp, - rootParams + rootParams, + u = u, + message_id = messageId ) } From 1845f4a15801942bca38596405d56d907b55c2a8 Mon Sep 17 00:00:00 2001 From: Marton Matusek Date: Tue, 7 May 2024 15:53:47 +0200 Subject: [PATCH 12/33] fix(inapp): use DialogFragment.showNow() instead of show to display overlay inapp SUITEDEV-35723 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: Andras Sarro --- .../mobileengage/iam/OverlayInAppPresenterTest.kt | 10 +++++----- .../emarsys/mobileengage/iam/OverlayInAppPresenter.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt index 95845e8b..04d6d54d 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt @@ -112,7 +112,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { }.start() countDownLatch.await() scenario.close() - verify(mockIamDialog).show(any(), any()) + verify(mockIamDialog).showNow(any(), any()) callbackCalled shouldBe true } @@ -138,7 +138,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { }.start() countDownLatch.await() - verify(mockIamDialog, times(0)).show(any(), any()) + verify(mockIamDialog, times(0)).showNow(any(), any()) callbackCalled shouldBe true } @@ -164,7 +164,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { }.start() countDownLatch.await() - verify(mockIamDialog, times(0)).show(any(), any()) + verify(mockIamDialog, times(0)).showNow(any(), any()) callbackCalled shouldBe true } @@ -206,7 +206,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { }.start() countDownLatch.await() - verify(mockIamDialog, times(0)).show(any(), any()) + verify(mockIamDialog, times(0)).showNow(any(), any()) callbackCalled shouldBe true } @@ -249,7 +249,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { }.start() countDownLatch.await() - verify(mockIamDialog, times(0)).show(any(), any()) + verify(mockIamDialog, times(0)).showNow(any(), any()) callbackCalled shouldBe true } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt index e67c52fa..af084f67 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt @@ -46,7 +46,7 @@ class OverlayInAppPresenter( ) ) if (!it.isStateSaved) { - iamDialog.show(it, IamDialog.TAG) + iamDialog.showNow(it, IamDialog.TAG) } } } From 32dacc65b84ef332df0be3fecc38cccef4852dbb Mon Sep 17 00:00:00 2001 From: LordAndras Date: Tue, 7 May 2024 16:10:40 +0200 Subject: [PATCH 13/33] fix(image-utils): add try-catch to loadBitmap and decoding SUITEDEV-35780 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- .../java/com/emarsys/core/util/ImageUtilsTest.kt | 5 ----- core/src/main/java/com/emarsys/core/util/ImageUtils.kt | 9 +++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/androidTest/java/com/emarsys/core/util/ImageUtilsTest.kt b/core/src/androidTest/java/com/emarsys/core/util/ImageUtilsTest.kt index 575d50ad..e671d1e9 100644 --- a/core/src/androidTest/java/com/emarsys/core/util/ImageUtilsTest.kt +++ b/core/src/androidTest/java/com/emarsys/core/util/ImageUtilsTest.kt @@ -82,13 +82,11 @@ class ImageUtilsTest : AnnotationSpec() { @Test - fun testLoadOptimizedBitmap_returnsNull_whenImageUrlIsNull() { ImageUtils.loadOptimizedBitmap(mockFileDownloader, null, deviceInfo) shouldBe null } @Test - fun testLoadOptimizedBitmap_withRemoteUrl_CleansUpTempFile() { clearCache() getTargetContext().cacheDir.list()?.size shouldBe 0 @@ -97,7 +95,6 @@ class ImageUtilsTest : AnnotationSpec() { } @Test - fun testLoadOptimizedBitmap_withLocalFile_ShouldNotCleanUpLocalFile() { clearCache() val fileUrl = mockFileDownloader.download(IMAGE_URL) @@ -113,7 +110,6 @@ class ImageUtilsTest : AnnotationSpec() { } @Test - fun testLoadOptimizedBitmap_withRemoteUrl() { val bitmap = ImageUtils.loadOptimizedBitmap(mockFileDownloader, IMAGE_URL, deviceInfo) bitmap shouldNotBe null @@ -122,7 +118,6 @@ class ImageUtilsTest : AnnotationSpec() { } @Test - fun testCalculateInSampleSize_returnedValueShouldBe4_whenRequestedWidthIs1080_widthIs2500() { val options = BitmapFactory.Options().apply { outWidth = 2500 diff --git a/core/src/main/java/com/emarsys/core/util/ImageUtils.kt b/core/src/main/java/com/emarsys/core/util/ImageUtils.kt index 1e45d790..2ba7b0bc 100644 --- a/core/src/main/java/com/emarsys/core/util/ImageUtils.kt +++ b/core/src/main/java/com/emarsys/core/util/ImageUtils.kt @@ -4,6 +4,8 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.webkit.URLUtil import com.emarsys.core.device.DeviceInfo +import com.emarsys.core.util.log.Logger +import com.emarsys.core.util.log.entry.CrashLog import java.io.File object ImageUtils { @@ -45,14 +47,17 @@ object ImageUtils { return URLUtil.isHttpsUrl(imageUrl) } - private fun loadBitmap(imageFileUrl: String, width: Int): Bitmap { + private fun loadBitmap(imageFileUrl: String, width: Int): Bitmap? = try { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeFile(imageFileUrl, options) options.inSampleSize = calculateInSampleSize(options, width) options.inJustDecodeBounds = false - return BitmapFactory.decodeFile(imageFileUrl, options) + BitmapFactory.decodeFile(imageFileUrl, options) + } catch (exception:Exception) { + Logger.error(CrashLog(exception)) + null } fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int): Int { From 5b7ac239718c316d19a5d37b257c3bc171f8a2cd Mon Sep 17 00:00:00 2001 From: LordAndras Date: Wed, 8 May 2024 15:50:15 +0200 Subject: [PATCH 14/33] chore(test-runner): add missing annotation to remove warnings SUITEDEV-35735 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> --- .../src/main/java/com/emarsys/testUtil/KotestRunnerAndroid.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testUtils/src/main/java/com/emarsys/testUtil/KotestRunnerAndroid.kt b/testUtils/src/main/java/com/emarsys/testUtil/KotestRunnerAndroid.kt index 3820e331..8709ebbb 100644 --- a/testUtils/src/main/java/com/emarsys/testUtil/KotestRunnerAndroid.kt +++ b/testUtils/src/main/java/com/emarsys/testUtil/KotestRunnerAndroid.kt @@ -1,5 +1,6 @@ package com.emarsys.testUtil +import io.kotest.common.KotestInternal import io.kotest.core.config.EmptyExtensionRegistry import io.kotest.core.config.ProjectConfiguration import io.kotest.core.spec.Spec @@ -12,6 +13,7 @@ import org.junit.runner.Description import org.junit.runner.Runner import org.junit.runner.notification.RunNotifier +@OptIn(KotestInternal::class) class KotestRunnerAndroid( private val kClass: Class ) : Runner() { From edcff21cc84234baff109e37086a01bae884e227 Mon Sep 17 00:00:00 2001 From: LordAndras Date: Wed, 8 May 2024 15:51:43 +0200 Subject: [PATCH 15/33] chore(version): lower min-sdk value to 24 SUITEDEV-35735 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ea4190b..63176e5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ androidx-navigation-compose = "2.7.6" compose-compiler = "1.5.6" compose-plugin = "1.6.0-dev1419" android-compileSdk = "34" -android-minSdk = "26" +android-minSdk = "24" android-targetSdk = "34" androidx-activity = "1.8.2" androidx-activityCompose = "1.8.2" From e207c3c169551e1d863dd09421f0d4f28e1abbe4 Mon Sep 17 00:00:00 2001 From: LordAndras Date: Wed, 8 May 2024 16:07:14 +0200 Subject: [PATCH 16/33] chore(release): 3.7.5 SUITEDEV-35735 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> --- changelog.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 543758b6..be9f3824 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ # What's fixed ### [Push](https://github.com/emartech/android-emarsys-sdk/wiki#2-push) -* Fixed issue where the Default Tap Actions' payload was not mapped correctly. \ No newline at end of file +* Fixed issue where the message id was not passed to the event handler. +* Fixed issue where the custom field was not passed to the event handler. + +* ### [Inapp](https://github.com/emartech/android-emarsys-sdk/wiki#3-inapp) +* Fixed issue where the showing of the inapp during an activity lifecycle transition caused a crash. + +* ### [Emarsys](https://github.com/emartech/android-emarsys-sdk/wiki#contents) +* Fixed issue where an image handling error could cause a crash. \ No newline at end of file From d062bff2ace95274a48fb96815c337c437abfb71 Mon Sep 17 00:00:00 2001 From: Marton Matusek <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 11:40:19 +0200 Subject: [PATCH 17/33] chore(codeql): create codeql.yml config SUITEDEV-35893 --- .github/workflows/codeql.yml | 107 +++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..7cfcce51 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,107 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "dev" ] + schedule: + - cron: '41 8 * * 3' + +env: + USE_LOCAL_DEPENDENCY: ${{ vars.USE_LOCAL_DEPENDENCY }} + RELEASE_MODE: ${{ vars.RELEASE_MODE }} + FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} + FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} + GOOGLE_OAUTH_SERVER_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_SERVER_CLIENT_ID }} + GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} + GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64 }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ubuntu-lates + timeout-minutes: 360 + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: java-kotlin + build-mode: manual + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Prepare CI + run: make prepare-ci + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + ./gradlew clean build -x test -x :sample:build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From 55ef481734e19a25d9c385a6af1e1dd450d4f0b8 Mon Sep 17 00:00:00 2001 From: matusekma <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 11:45:22 +0200 Subject: [PATCH 18/33] chore(codeql): fix runner name SUITEDEV-35893 --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7cfcce51..a8471af7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ubuntu-lates + runs-on: ubuntu-latest timeout-minutes: 360 permissions: # required for all workflows From 316046b278248222be24b342a2364d675ea1f22d Mon Sep 17 00:00:00 2001 From: matusekma <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 12:47:11 +0200 Subject: [PATCH 19/33] chore(codeql): add missing env variables SUITEDEV-35893 --- .github/workflows/codeql.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a8471af7..c4db94cf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,6 +22,10 @@ on: env: USE_LOCAL_DEPENDENCY: ${{ vars.USE_LOCAL_DEPENDENCY }} RELEASE_MODE: ${{ vars.RELEASE_MODE }} + ANDROID_RELEASE_STORE_FILE_BASE64: ${{ secrets.ANDROID_RELEASE_STORE_FILE_BASE64 }} + ANDROID_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }} + ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} + ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} GOOGLE_OAUTH_SERVER_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_SERVER_CLIENT_ID }} @@ -29,6 +33,10 @@ env: GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64 }} OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SONATYPE_SIGNING_KEY_ID: ${{ secrets.SONATYPE_SIGNING_KEY_ID }} + SONATYPE_SIGNING_PASSWORD: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} + SONATYPE_SIGNING_SECRET_KEY_RING_FILE_BASE64: ${{ secrets.SONATYPE_SIGNING_SECRET_KEY_RING_FILE_BASE64 }} jobs: analyze: From d6b885144ee0a378ee5aa194fa56623b60ab6408 Mon Sep 17 00:00:00 2001 From: matusekma <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 13:13:47 +0200 Subject: [PATCH 20/33] chore(codeql): change default versionCode to 1 SUITEDEV-35893 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index ee905b94..827ca933 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,7 +49,7 @@ fun versionData() { } catch (ignored: Exception) { GitVersion( versionName = "0.0.0", - versionCode = 0, + versionCode = 1, versionCodeTime = 0 ) } From 1c64348ebe420e14708b65f1604c283e338c0e71 Mon Sep 17 00:00:00 2001 From: matusekma <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 11:45:22 +0200 Subject: [PATCH 21/33] chore(codeql): fix runner name SUITEDEV-35893 --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7cfcce51..a8471af7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ubuntu-lates + runs-on: ubuntu-latest timeout-minutes: 360 permissions: # required for all workflows From 0dc05bd7fb0fbf2966c4ccad28399366f14ee51b Mon Sep 17 00:00:00 2001 From: matusekma <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 12:47:11 +0200 Subject: [PATCH 22/33] chore(codeql): add missing env variables SUITEDEV-35893 --- .github/workflows/codeql.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a8471af7..c4db94cf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,6 +22,10 @@ on: env: USE_LOCAL_DEPENDENCY: ${{ vars.USE_LOCAL_DEPENDENCY }} RELEASE_MODE: ${{ vars.RELEASE_MODE }} + ANDROID_RELEASE_STORE_FILE_BASE64: ${{ secrets.ANDROID_RELEASE_STORE_FILE_BASE64 }} + ANDROID_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }} + ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} + ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} GOOGLE_OAUTH_SERVER_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_SERVER_CLIENT_ID }} @@ -29,6 +33,10 @@ env: GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_STORE_SEVICE_ACCOUNT_JSON_BASE64 }} OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SONATYPE_SIGNING_KEY_ID: ${{ secrets.SONATYPE_SIGNING_KEY_ID }} + SONATYPE_SIGNING_PASSWORD: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} + SONATYPE_SIGNING_SECRET_KEY_RING_FILE_BASE64: ${{ secrets.SONATYPE_SIGNING_SECRET_KEY_RING_FILE_BASE64 }} jobs: analyze: From b60935a3a1f22e07eeba6ef3c62cb76ced761e42 Mon Sep 17 00:00:00 2001 From: matusekma <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 13:13:47 +0200 Subject: [PATCH 23/33] chore(codeql): change default versionCode to 1 SUITEDEV-35893 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index ee905b94..827ca933 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,7 +49,7 @@ fun versionData() { } catch (ignored: Exception) { GitVersion( versionName = "0.0.0", - versionCode = 0, + versionCode = 1, versionCodeTime = 0 ) } From ed98b9686ad799cf3188339cc48e97187b64fda9 Mon Sep 17 00:00:00 2001 From: matusekma <36794575+matusekma@users.noreply.github.com> Date: Tue, 21 May 2024 13:32:14 +0200 Subject: [PATCH 24/33] chore(codeql): add master to code scanning SUITEDEV-35893 --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c4db94cf..14c3a19d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,9 @@ name: "CodeQL" on: push: - branches: [ "dev" ] + branches: [ "dev", "master" ] pull_request: - branches: [ "dev" ] + branches: [ "dev", "master" ] schedule: - cron: '41 8 * * 3' From 15ac5c0d3ff7e06f2971b40e0ca7edbb48292260 Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Wed, 3 Jul 2024 12:06:59 +0200 Subject: [PATCH 25/33] feat(hardware-id): widen the exceptions caught from database SUITEDEV-36168 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> --- .../com/emarsys/core/provider/hardwareid/HardwareIdProvider.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/com/emarsys/core/provider/hardwareid/HardwareIdProvider.kt b/core/src/main/java/com/emarsys/core/provider/hardwareid/HardwareIdProvider.kt index cf31a82d..94d9f276 100644 --- a/core/src/main/java/com/emarsys/core/provider/hardwareid/HardwareIdProvider.kt +++ b/core/src/main/java/com/emarsys/core/provider/hardwareid/HardwareIdProvider.kt @@ -1,6 +1,5 @@ package com.emarsys.core.provider.hardwareid -import android.database.CursorIndexOutOfBoundsException import com.emarsys.core.Mockable import com.emarsys.core.contentresolver.hardwareid.HardwareIdContentResolver import com.emarsys.core.crypto.HardwareIdentificationCrypto @@ -26,7 +25,7 @@ class HardwareIdProvider( fun provideHardwareId(): String { val hardware: HardwareIdentification? = try { repository.query(Everything()).firstOrNull() - } catch (error: CursorIndexOutOfBoundsException) { + } catch (error: Exception) { val status = mutableMapOf() status["message"] = error.message ?: "" status["stackTrace"] = error.stackTrace.map { it.toString() } From 58567ce5137a07c93987fc0cdaf286b5b68ad91e Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Wed, 10 Jul 2024 14:55:56 +0200 Subject: [PATCH 26/33] chore(release): 3.7.6 SUITEDEV-36111 Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- changelog.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/changelog.md b/changelog.md index be9f3824..3d721438 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,3 @@ -# What's fixed -### [Push](https://github.com/emartech/android-emarsys-sdk/wiki#2-push) -* Fixed issue where the message id was not passed to the event handler. -* Fixed issue where the custom field was not passed to the event handler. - -* ### [Inapp](https://github.com/emartech/android-emarsys-sdk/wiki#3-inapp) -* Fixed issue where the showing of the inapp during an activity lifecycle transition caused a crash. - -* ### [Emarsys](https://github.com/emartech/android-emarsys-sdk/wiki#contents) -* Fixed issue where an image handling error could cause a crash. \ No newline at end of file +# What's changed +### [Emarsys](https://github.com/emartech/android-emarsys-sdk/wiki#contents) +* Changed error handling in database operations to receive more detailed information. \ No newline at end of file From adbaff465643e16c6b20744dbecdd179852d836b Mon Sep 17 00:00:00 2001 From: LasOri Date: Wed, 24 Jul 2024 14:02:52 +0200 Subject: [PATCH 27/33] feat(predict): Add percent encoding to parameters SUITEDEV-36140 Co-authored-by: matusekma Co-authored-by: Andras Sarro --- .../java/com/emarsys/PredictIntegrationTest.kt | 12 ++++++++++++ .../emarsys/predict/api/model/RecommendationLogic.kt | 8 +++++--- .../com/emarsys/predict/DefaultPredictInternal.kt | 5 +++-- .../java/com/emarsys/predict/util/CartItemUtils.java | 5 ++++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt index 1f1b1af9..6ed0ff22 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt @@ -217,6 +217,18 @@ class PredictIntegrationTest : AnnotationSpec() { eventuallyAssertSuccess() } + @Test + fun testTrackItemView_withUrlEncodableCharacter() { + val itemId = "2508+" + responseModelMatches = { + it.requestModel.url.toString().contains("v=i%3A2508%252B") + } + + Emarsys.predict.trackItemView(itemId) + + eventuallyAssertSuccess() + } + @Test fun testTrackItemView_withProduct() { val product = Product(ITEM3, "TestTitle", "https://emarsys.com", "RELATED", "AAAA") diff --git a/predict-api/src/main/java/com/emarsys/predict/api/model/RecommendationLogic.kt b/predict-api/src/main/java/com/emarsys/predict/api/model/RecommendationLogic.kt index cb6619cc..7ef2b620 100644 --- a/predict-api/src/main/java/com/emarsys/predict/api/model/RecommendationLogic.kt +++ b/predict-api/src/main/java/com/emarsys/predict/api/model/RecommendationLogic.kt @@ -1,5 +1,7 @@ package com.emarsys.predict.api.model +import java.net.URLEncoder + class RecommendationLogic internal constructor(override val logicName: String, override val data: Map = mapOf(), override val variants: List = listOf()) : Logic { companion object { @@ -47,7 +49,7 @@ class RecommendationLogic internal constructor(override val logicName: String, o @JvmStatic fun related(itemId: String): Logic { - val data = mapOf("v" to "i:$itemId") + val data = mapOf("v" to "i:${URLEncoder.encode(itemId, Charsets.UTF_8)}") return RecommendationLogic(RELATED, data, listOf()) } @@ -71,7 +73,7 @@ class RecommendationLogic internal constructor(override val logicName: String, o @JvmStatic fun alsoBought(itemId: String): Logic { - val data = mapOf("v" to "i:$itemId") + val data = mapOf("v" to "i:${URLEncoder.encode(itemId, Charsets.UTF_8)}") return RecommendationLogic(ALSO_BOUGHT, data, listOf()) } @@ -111,7 +113,7 @@ class RecommendationLogic internal constructor(override val logicName: String, o } private fun cartItemToQueryParam(cartItem: CartItem?): String { - return "i:" + cartItem!!.itemId + ",p:" + cartItem.price + ",q:" + cartItem.quantity + return "i:" + URLEncoder.encode(cartItem!!.itemId, Charsets.UTF_8) + ",p:" + cartItem.price + ",q:" + cartItem.quantity } } } \ No newline at end of file diff --git a/predict/src/main/java/com/emarsys/predict/DefaultPredictInternal.kt b/predict/src/main/java/com/emarsys/predict/DefaultPredictInternal.kt index ac3eb72a..acff2354 100644 --- a/predict/src/main/java/com/emarsys/predict/DefaultPredictInternal.kt +++ b/predict/src/main/java/com/emarsys/predict/DefaultPredictInternal.kt @@ -23,6 +23,7 @@ import com.emarsys.predict.model.LastTrackedItemContainer import com.emarsys.predict.provider.PredictRequestModelBuilderProvider import com.emarsys.predict.request.PredictRequestContext import com.emarsys.predict.util.CartItemUtils +import java.net.URLEncoder class DefaultPredictInternal( requestContext: PredictRequestContext, @@ -89,7 +90,7 @@ class DefaultPredictInternal( override fun trackItemView(itemId: String): String { val shard = ShardModel.Builder(timestampProvider, uuidProvider) .type(TYPE_ITEM_VIEW) - .payloadEntry("v", "i:$itemId") + .payloadEntry("v", "i:${URLEncoder.encode(itemId, Charsets.UTF_8)}") .build() requestManager.submit(shard) lastTrackedContainer.lastItemView = itemId @@ -178,7 +179,7 @@ class DefaultPredictInternal( .type(TYPE_ITEM_VIEW) .payloadEntry( "v", - "i:" + product.productId + ",t:" + product.feature + ",c:" + product.cohort + "i:" + URLEncoder.encode(product.productId, Charsets.UTF_8) + ",t:" + product.feature + ",c:" + product.cohort ) .build() requestManager.submit(shard) diff --git a/predict/src/main/java/com/emarsys/predict/util/CartItemUtils.java b/predict/src/main/java/com/emarsys/predict/util/CartItemUtils.java index 8275e1bf..d84c0bbd 100644 --- a/predict/src/main/java/com/emarsys/predict/util/CartItemUtils.java +++ b/predict/src/main/java/com/emarsys/predict/util/CartItemUtils.java @@ -3,8 +3,11 @@ import com.emarsys.core.util.Assert; import com.emarsys.predict.api.model.CartItem; +import java.net.URLEncoder; import java.util.List; +import kotlin.text.Charsets; + public class CartItemUtils { public static String cartItemsToQueryParam(List items) { @@ -24,7 +27,7 @@ public static String cartItemsToQueryParam(List items) { } private static String cartItemToQueryParam(CartItem cartItem) { - return "i:" + cartItem.getItemId() + ",p:" + cartItem.getPrice() + ",q:" + cartItem.getQuantity(); + return "i:" + URLEncoder.encode(cartItem.getItemId(), Charsets.UTF_8) + ",p:" + cartItem.getPrice() + ",q:" + cartItem.getQuantity(); } } From 5ea23a054cb21c5a5bd514ce0139ad8d5a5b3780 Mon Sep 17 00:00:00 2001 From: LasOri Date: Wed, 24 Jul 2024 14:03:09 +0200 Subject: [PATCH 28/33] feat(sample): Add recommendationClick to sample SUITEDEV-36140 Co-authored-by: matusekma Co-authored-by: Andras Sarro --- .../kotlin/com/emarsys/sample/predict/PredictScreen.kt | 10 ++++++++++ .../com/emarsys/sample/predict/PredictViewModel.kt | 3 ++- sample/src/main/res/values/strings.xml | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt index 90ec559a..475ad4ba 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -140,6 +142,14 @@ class PredictScreen( } } item { GreyLine() } + item { + TextButton(onClick = { + Emarsys.predict.trackRecommendationClick(viewModel.product.value) + }) { + Text(stringResource(id = R.string.track_recommendation_click)) + } + } + item { GreyLine() } item { TitleText(titleText = stringResource(id = R.string.recommend_text)) } diff --git a/sample/src/main/kotlin/com/emarsys/sample/predict/PredictViewModel.kt b/sample/src/main/kotlin/com/emarsys/sample/predict/PredictViewModel.kt index 63cdf299..72b8a997 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/predict/PredictViewModel.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/predict/PredictViewModel.kt @@ -17,6 +17,7 @@ class PredictViewModel : ViewModel() { val categoryView = mutableStateOf("") val searchTerm = mutableStateOf("") val orderId = mutableStateOf("") + val product = mutableStateOf(Product("test+ID1", "testTitle", "https://emarsys.com", "RELATED", "AAAA")) val recommendedProducts = mutableStateListOf() val sampleCart = mutableStateListOf() val recommendationLogic = mutableStateOf(RecommendationLogic.search(searchTerm.value)) @@ -82,7 +83,7 @@ class PredictViewModel : ViewModel() { private fun generateCartItem(): SampleCartItem { return SampleCartItem( - itemId = Random().nextInt().toString(), + itemId = Random().nextInt().toString()+"+testId", price = Random().nextDouble(), quantity = Random().nextDouble() ) diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index d2de0d87..89bda121 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -60,7 +60,9 @@ Recommend Recommendations Track order + Track recommendation click orderId + product Track search searchTerm Track category From dcf5d5dc74624ccce523ae267a6b8c3bf45243e5 Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Wed, 24 Jul 2024 15:15:32 +0200 Subject: [PATCH 29/33] test(predict-internal): use cartItem instead of mock SUITEDEV-36140 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- .../java/com/emarsys/predict/DefaultPredictInternalTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/predict/src/androidTest/java/com/emarsys/predict/DefaultPredictInternalTest.kt b/predict/src/androidTest/java/com/emarsys/predict/DefaultPredictInternalTest.kt index b380b37f..d161d609 100644 --- a/predict/src/androidTest/java/com/emarsys/predict/DefaultPredictInternalTest.kt +++ b/predict/src/androidTest/java/com/emarsys/predict/DefaultPredictInternalTest.kt @@ -204,7 +204,8 @@ class DefaultPredictInternalTest : AnnotationSpec() { @Test fun testTrackPurchase_returnsShardId() { - predictInternal.trackPurchase("orderId", listOf(mock())) shouldBe ID1 + val testCartItem = PredictCartItem(ID1, 200.0, 100.0) + predictInternal.trackPurchase("orderId", listOf(testCartItem)) shouldBe ID1 } @Test From 0f555bd9db36089144d5c9a07927eeab41b4823c Mon Sep 17 00:00:00 2001 From: LasOri Date: Wed, 31 Jul 2024 11:39:58 +0200 Subject: [PATCH 30/33] feat(iam): use transition safe activity watchdog for inapp presentation SUITEDEV-36239 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> --- .../core/di/FakeCoreDependencyContainer.kt | 2 + .../TransitionSafeCurrentActivityWatchdog.kt | 94 +++++++++++++++++++ .../java/com/emarsys/core/di/CoreComponent.kt | 3 + .../com/emarsys/core/handler/SdkHandler.kt | 4 + .../com/emarsys/core/observer/Observer.kt | 9 ++ .../fake/FakeFirebaseDependencyContainer.kt | 4 +- .../fake/FakeHuaweiDependencyContainer.kt | 4 +- .../java/com/emarsys/EmarsysTest.kt | 2 +- .../com/emarsys/di/FakeDependencyContainer.kt | 4 +- .../src/main/java/com/emarsys/Emarsys.kt | 1 + .../com/emarsys/di/DefaultEmarsysComponent.kt | 12 ++- .../fake/FakeEmarsysDependencyContainer.kt | 2 + .../FakeMobileEngageDependencyContainer.kt | 4 +- .../iam/OverlayInAppPresenterTest.kt | 43 ++------- .../mobileengage/iam/dialog/IamDialogTest.kt | 41 ++++++-- .../iam/webview/IamWebViewFactoryTest.kt | 15 ++- .../InAppMessageResponseHandlerTest.kt | 5 +- .../mobileengage/iam/OverlayInAppPresenter.kt | 48 +++++----- .../mobileengage/iam/dialog/IamDialog.kt | 16 +++- .../iam/webview/IamWebViewFactory.kt | 8 +- 20 files changed, 232 insertions(+), 89 deletions(-) create mode 100644 core/src/main/java/com/emarsys/core/activity/TransitionSafeCurrentActivityWatchdog.kt create mode 100644 core/src/main/java/com/emarsys/core/observer/Observer.kt diff --git a/core/src/androidTest/java/com/emarsys/core/di/FakeCoreDependencyContainer.kt b/core/src/androidTest/java/com/emarsys/core/di/FakeCoreDependencyContainer.kt index 90cdd24a..f00e161e 100644 --- a/core/src/androidTest/java/com/emarsys/core/di/FakeCoreDependencyContainer.kt +++ b/core/src/androidTest/java/com/emarsys/core/di/FakeCoreDependencyContainer.kt @@ -5,6 +5,7 @@ import com.emarsys.core.CoreCompletionHandler import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.connection.ConnectionWatchDog import com.emarsys.core.crypto.Crypto @@ -54,4 +55,5 @@ class FakeCoreDependencyContainer( override val coreCompletionHandler: CoreCompletionHandler = mock(), override val logLevelStorage: Storage = mock(), override val activityLifecycleActionRegistry: ActivityLifecycleActionRegistry = mock(), + override val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog = mock(), ) : CoreComponent \ No newline at end of file diff --git a/core/src/main/java/com/emarsys/core/activity/TransitionSafeCurrentActivityWatchdog.kt b/core/src/main/java/com/emarsys/core/activity/TransitionSafeCurrentActivityWatchdog.kt new file mode 100644 index 00000000..6aa064b7 --- /dev/null +++ b/core/src/main/java/com/emarsys/core/activity/TransitionSafeCurrentActivityWatchdog.kt @@ -0,0 +1,94 @@ +package com.emarsys.core.activity + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import com.emarsys.core.Mockable +import com.emarsys.core.handler.SdkHandler +import com.emarsys.core.observer.Observer +import java.util.concurrent.CountDownLatch + +@Mockable +class TransitionSafeCurrentActivityWatchdog(private val handler: SdkHandler) : + ActivityLifecycleCallbacks, Observer { + + private val activityCallbacks = mutableListOf<(Activity) -> Unit>() + + private var currentActivity: Activity? = null + + private val callback: Runnable = Runnable { + if (currentActivity != null) { + notify(currentActivity!!) + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + currentActivity = null + } + + override fun onActivityStarted(activity: Activity) { + currentActivity = null + } + + override fun onActivityResumed(activity: Activity) { + currentActivity = activity + handler.postDelayed(callback, 500) + } + + override fun onActivityPaused(activity: Activity) { + if (activity == currentActivity) { + currentActivity = null + handler.remove(callback) + } + } + + override fun onActivityStopped(activity: Activity) { + if (activity == currentActivity) { + currentActivity = null + handler.remove(callback) + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + if (activity == currentActivity) { + currentActivity = null + handler.remove(callback) + } + } + + override fun onActivityDestroyed(activity: Activity) { + if (activity == currentActivity) { + currentActivity = null + handler.remove(callback) + } + } + + override fun register(callback: (Activity) -> Unit) { + activityCallbacks.add(callback) + if (currentActivity != null) { + callback(currentActivity!!) + } + } + + override fun unregister(callback: (Activity) -> Unit) { + activityCallbacks.remove(callback) + } + + override fun notify(value: Activity) { + activityCallbacks.forEach { it(value) } + } + + fun activity(): Activity { + lateinit var result: Activity + val latch = CountDownLatch(1) + val callback: (Activity) -> Unit = { + result = it + latch.countDown() + } + register(callback) + latch.await() + unregister(callback) + return result + } + +} diff --git a/core/src/main/java/com/emarsys/core/di/CoreComponent.kt b/core/src/main/java/com/emarsys/core/di/CoreComponent.kt index 53013218..5299bec0 100644 --- a/core/src/main/java/com/emarsys/core/di/CoreComponent.kt +++ b/core/src/main/java/com/emarsys/core/di/CoreComponent.kt @@ -5,6 +5,7 @@ import com.emarsys.core.CoreCompletionHandler import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.connection.ConnectionWatchDog import com.emarsys.core.crypto.Crypto import com.emarsys.core.database.CoreSQLiteDatabase @@ -97,4 +98,6 @@ interface CoreComponent { val connectionWatchdog: ConnectionWatchDog val coreCompletionHandler: CoreCompletionHandler + + val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog } \ No newline at end of file diff --git a/core/src/main/java/com/emarsys/core/handler/SdkHandler.kt b/core/src/main/java/com/emarsys/core/handler/SdkHandler.kt index 59579484..20a80ca7 100644 --- a/core/src/main/java/com/emarsys/core/handler/SdkHandler.kt +++ b/core/src/main/java/com/emarsys/core/handler/SdkHandler.kt @@ -13,4 +13,8 @@ class SdkHandler(val handler: Handler) { handler.postDelayed(runnable, delay) } + fun remove(runnable: Runnable) { + handler.removeCallbacks(runnable) + } + } \ No newline at end of file diff --git a/core/src/main/java/com/emarsys/core/observer/Observer.kt b/core/src/main/java/com/emarsys/core/observer/Observer.kt new file mode 100644 index 00000000..ef2d0cbc --- /dev/null +++ b/core/src/main/java/com/emarsys/core/observer/Observer.kt @@ -0,0 +1,9 @@ +package com.emarsys.core.observer + +interface Observer { + + fun register(callback: (T) -> Unit) + fun unregister(callback: (T) -> Unit) + fun notify(value: T) + +} \ No newline at end of file diff --git a/emarsys-firebase/src/androidTest/java/com/emarsys/fake/FakeFirebaseDependencyContainer.kt b/emarsys-firebase/src/androidTest/java/com/emarsys/fake/FakeFirebaseDependencyContainer.kt index 5f2e076c..410e5b00 100644 --- a/emarsys-firebase/src/androidTest/java/com/emarsys/fake/FakeFirebaseDependencyContainer.kt +++ b/emarsys-firebase/src/androidTest/java/com/emarsys/fake/FakeFirebaseDependencyContainer.kt @@ -7,6 +7,7 @@ import com.emarsys.core.CoreCompletionHandler import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.app.AppLifecycleObserver import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.connection.ConnectionWatchDog @@ -154,5 +155,6 @@ class FakeFirebaseDependencyContainer( override val jsCommandFactoryProvider: JSCommandFactoryProvider = mock(), override val jsOnCloseListener: OnCloseListener = mock(), override val jsOnAppEventListener: OnAppEventListener = mock(), - override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock() + override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock(), + override val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog = mock(), ) : MobileEngageComponent \ No newline at end of file diff --git a/emarsys-huawei/src/androidTest/java/com/emarsys/fake/FakeHuaweiDependencyContainer.kt b/emarsys-huawei/src/androidTest/java/com/emarsys/fake/FakeHuaweiDependencyContainer.kt index 24f0774f..82d8bcfe 100644 --- a/emarsys-huawei/src/androidTest/java/com/emarsys/fake/FakeHuaweiDependencyContainer.kt +++ b/emarsys-huawei/src/androidTest/java/com/emarsys/fake/FakeHuaweiDependencyContainer.kt @@ -7,6 +7,7 @@ import com.emarsys.core.CoreCompletionHandler import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.app.AppLifecycleObserver import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.connection.ConnectionWatchDog @@ -154,5 +155,6 @@ class FakeHuaweiDependencyContainer( override val jsCommandFactoryProvider: JSCommandFactoryProvider = mock(), override val jsOnCloseListener: OnCloseListener = mock(), override val jsOnAppEventListener: OnAppEventListener = mock(), - override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock() + override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock(), + override val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog = mock(), ) : MobileEngageComponent \ No newline at end of file diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt index 9c1d2686..d2196235 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt @@ -505,7 +505,7 @@ class EmarsysTest : AnnotationSpec() { setup(mobileEngageConfig) runBlockingOnCoreSdkThread { - verify(application, times(2)).registerActivityLifecycleCallbacks(capture()) + verify(application, times(3)).registerActivityLifecycleCallbacks(capture()) } } } diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/di/FakeDependencyContainer.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/di/FakeDependencyContainer.kt index f6bb7f66..9437d034 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/di/FakeDependencyContainer.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/di/FakeDependencyContainer.kt @@ -10,6 +10,7 @@ import com.emarsys.core.CoreCompletionHandler import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.app.AppLifecycleObserver import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.connection.ConnectionWatchDog @@ -200,5 +201,6 @@ class FakeDependencyContainer( override val jsCommandFactoryProvider: JSCommandFactoryProvider = mock(), override val jsOnCloseListener: OnCloseListener = mock(), override val jsOnAppEventListener: OnAppEventListener = mock(), - override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock() + override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock(), + override val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog = mock() ) : EmarsysComponent \ No newline at end of file diff --git a/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt b/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt index fc0ea64b..acfa3760 100644 --- a/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt +++ b/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt @@ -192,6 +192,7 @@ object Emarsys { private fun registerWatchDogs(config: EmarsysConfig) { config.application.registerActivityLifecycleCallbacks(emarsys().currentActivityWatchdog) config.application.registerActivityLifecycleCallbacks(emarsys().activityLifecycleWatchdog) + config.application.registerActivityLifecycleCallbacks(emarsys().transitionSafeCurrentActivityWatchdog) } private fun registerDatabaseTriggers() { diff --git a/emarsys-sdk/src/main/java/com/emarsys/di/DefaultEmarsysComponent.kt b/emarsys-sdk/src/main/java/com/emarsys/di/DefaultEmarsysComponent.kt index 35511447..b37f149e 100644 --- a/emarsys-sdk/src/main/java/com/emarsys/di/DefaultEmarsysComponent.kt +++ b/emarsys-sdk/src/main/java/com/emarsys/di/DefaultEmarsysComponent.kt @@ -25,6 +25,7 @@ import com.emarsys.core.Mapper import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.api.notification.NotificationSettings import com.emarsys.core.api.proxyApi import com.emarsys.core.app.AppLifecycleObserver @@ -312,6 +313,12 @@ open class DefaultEmarsysComponent(config: EmarsysConfig) : EmarsysComponent { ) as ClipboardManager } + override val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog by lazy { + TransitionSafeCurrentActivityWatchdog( + concurrentHandlerHolder.coreHandler + ) + } + override val overlayInAppPresenter: OverlayInAppPresenter by lazy { OverlayInAppPresenter( concurrentHandlerHolder, @@ -323,7 +330,7 @@ open class DefaultEmarsysComponent(config: EmarsysConfig) : EmarsysComponent { webViewFactory ), timestampProvider, - currentActivityProvider + transitionSafeCurrentActivityWatchdog ) } @@ -749,8 +756,7 @@ open class DefaultEmarsysComponent(config: EmarsysConfig) : EmarsysComponent { IamWebViewFactory( iamJsBridgeFactory, jsCommandFactoryProvider, - concurrentHandlerHolder, - currentActivityProvider + concurrentHandlerHolder ) } diff --git a/emarsys/src/androidTest/java/com/emarsys/fake/FakeEmarsysDependencyContainer.kt b/emarsys/src/androidTest/java/com/emarsys/fake/FakeEmarsysDependencyContainer.kt index c5d066a7..5a6f6275 100644 --- a/emarsys/src/androidTest/java/com/emarsys/fake/FakeEmarsysDependencyContainer.kt +++ b/emarsys/src/androidTest/java/com/emarsys/fake/FakeEmarsysDependencyContainer.kt @@ -8,6 +8,7 @@ import com.emarsys.core.CoreCompletionHandler import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.app.AppLifecycleObserver import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.connection.ConnectionWatchDog @@ -156,4 +157,5 @@ class FakeEmarsysDependencyContainer( override val jsOnCloseListener: OnCloseListener = mock(), override val jsOnAppEventListener: OnAppEventListener = mock(), override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock(), + override val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog = mock(), ) : MobileEngageComponent \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/fake/FakeMobileEngageDependencyContainer.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/fake/FakeMobileEngageDependencyContainer.kt index 27b498eb..2bdbc04d 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/fake/FakeMobileEngageDependencyContainer.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/fake/FakeMobileEngageDependencyContainer.kt @@ -7,6 +7,7 @@ import com.emarsys.core.CoreCompletionHandler import com.emarsys.core.activity.ActivityLifecycleActionRegistry import com.emarsys.core.activity.ActivityLifecycleWatchdog import com.emarsys.core.activity.CurrentActivityWatchdog +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.app.AppLifecycleObserver import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.connection.ConnectionWatchDog @@ -154,5 +155,6 @@ class FakeMobileEngageDependencyContainer( override val jsCommandFactoryProvider: JSCommandFactoryProvider = mock(), override val jsOnCloseListener: OnCloseListener = mock(), override val jsOnAppEventListener: OnAppEventListener = mock(), - override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock() + override val remoteMessageMapperFactory: RemoteMessageMapperFactory = mock(), + override val transitionSafeCurrentActivityWatchdog: TransitionSafeCurrentActivityWatchdog = mock(), ): MobileEngageComponent \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt index 04d6d54d..36072d82 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/OverlayInAppPresenterTest.kt @@ -5,9 +5,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.test.core.app.ActivityScenario +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.handler.ConcurrentHandlerHolder -import com.emarsys.core.provider.activity.CurrentActivityProvider import com.emarsys.core.provider.timestamp.TimestampProvider import com.emarsys.mobileengage.iam.dialog.IamDialog import com.emarsys.mobileengage.iam.dialog.IamDialogProvider @@ -33,7 +33,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { private lateinit var concurrentHandlerHolder: ConcurrentHandlerHolder private lateinit var mockIamDialogProvider: IamDialogProvider private lateinit var mockTimestampProvider: TimestampProvider - private lateinit var mockCurrentActivityProvider: CurrentActivityProvider + private lateinit var mockCurrentActivityProvider: TransitionSafeCurrentActivityWatchdog private lateinit var mockIamDialog: IamDialog private lateinit var inAppPresenter: OverlayInAppPresenter @@ -48,7 +48,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { mockTimestampProvider = mock() mockCurrentActivityProvider = mock() - whenever(mockIamDialog.loadInApp(any(), any(), any())).thenAnswer { + whenever(mockIamDialog.loadInApp(any(), any(), any(), any())).thenAnswer { (it.getArgument(2) as MessageLoadedListener).onMessageLoaded() } @@ -56,7 +56,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { concurrentHandlerHolder, mockIamDialogProvider, mockTimestampProvider, - mockCurrentActivityProvider + mockCurrentActivityProvider, ) } @@ -67,7 +67,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { var callbackCalled = false Thread { scenario.onActivity { activity -> - whenever(mockCurrentActivityProvider.get()).thenReturn(activity) + whenever(mockCurrentActivityProvider.activity()).thenReturn(activity) inAppPresenter.present( "1", @@ -95,7 +95,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { var callbackCalled = false Thread { scenario.onActivity { activity -> - whenever(mockCurrentActivityProvider.get()).thenReturn(activity) + whenever(mockCurrentActivityProvider.activity()).thenReturn(activity) inAppPresenter.present( "1", @@ -122,7 +122,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { val countDownLatch = CountDownLatch(1) var callbackCalled = false Thread { - whenever(mockCurrentActivityProvider.get()).thenReturn(activity) + whenever(mockCurrentActivityProvider.activity()).thenReturn(activity) inAppPresenter.present( "1", @@ -143,31 +143,6 @@ class OverlayInAppPresenterTest : AnnotationSpec() { } - @Test - fun testPresent_shouldNotShowDialog_whenActivity_isNull() { - val countDownLatch = CountDownLatch(1) - var callbackCalled = false - Thread { - whenever(mockCurrentActivityProvider.get()).thenReturn(null) - - inAppPresenter.present( - "1", - SID, - URL, - "requestId", - 0L, - "

Hello

" - ) { - callbackCalled = true - countDownLatch.countDown() - } - }.start() - countDownLatch.await() - - verify(mockIamDialog, times(0)).showNow(any(), any()) - callbackCalled shouldBe true - } - @Test fun testPresent_shouldNotShowDialog_whenAnotherDialog_isAlreadyShown() { val activity: AppCompatActivity = mock() @@ -186,7 +161,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { ).thenReturn( mockIamDialog ) - whenever(mockCurrentActivityProvider.get()).thenReturn(activity) + whenever(mockCurrentActivityProvider.activity()).thenReturn(activity) whenever(activity.supportFragmentManager).thenReturn(fragmentManager) whenever(fragmentManager.findFragmentByTag("MOBILE_ENGAGE_IAM_DIALOG_TAG")).thenReturn( fragment @@ -228,7 +203,7 @@ class OverlayInAppPresenterTest : AnnotationSpec() { ).thenReturn( mockIamDialog ) - whenever(mockCurrentActivityProvider.get()).thenReturn(activity) + whenever(mockCurrentActivityProvider.activity()).thenReturn(activity) whenever(activity.supportFragmentManager).thenReturn(fragmentManager) whenever(fragmentManager.isStateSaved).thenReturn(true) whenever(fragmentManager.findFragmentByTag("MOBILE_ENGAGE_IAM_DIALOG_TAG")).thenReturn( diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt index a87657ce..7b455040 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt @@ -91,7 +91,7 @@ class IamDialogTest : AnnotationSpec() { val iamWebView = createWebView() mockWebViewFactory = mock { - on { create(null) } doReturn iamWebView + on { create(mock()) } doReturn iamWebView } setupMobileEngageComponent( @@ -408,18 +408,23 @@ class IamDialogTest : AnnotationSpec() { val args = Bundle() args.putString(CAMPAIGN_ID_KEY, "123456789") val actions: List = listOf(mock(), mock(), mock()) + + val fragmentLatch = CountDownLatch(1) val fragmentScenario = launchFragment(args) { iamDialog.apply { setActions(actions) } } - fragmentScenario.onFragment { - displayDialog(fragmentScenario) + displayDialog(fragmentScenario) + + fragmentScenario.onFragment { for (action in actions) { verify(action).execute("123456789", null, null) } + fragmentLatch.countDown() } + fragmentLatch.await() } @Test @@ -440,6 +445,14 @@ class IamDialogTest : AnnotationSpec() { fragmentScenario.moveToState(Lifecycle.State.CREATED) fragmentScenario.moveToState(Lifecycle.State.RESUMED) fragmentScenario.onFragment { + it.activity?.let { activity -> + iamDialog.loadInApp( + "", + InAppMetaData("123456789", null, null), + MessageLoadedListener {}, + activity + ) + } for (action in actions) { verify(action, times(1)).execute(any(), any(), any()) } @@ -458,6 +471,14 @@ class IamDialogTest : AnnotationSpec() { } fragmentScenario.onFragment { + it.activity?.let { activity -> + iamDialog.loadInApp( + "", + InAppMetaData("123456789", null, null), + MessageLoadedListener {}, + activity + ) + } it.activity?.runOnUiThread { it.onPause() } @@ -481,6 +502,14 @@ class IamDialogTest : AnnotationSpec() { iamDialog } fragmentScenario.onFragment { + it.activity?.let { activity -> + iamDialog.loadInApp( + "", + InAppMetaData("123456789", null, null), + MessageLoadedListener {}, + activity + ) + } it.activity?.runOnUiThread { it.onPause() } @@ -526,7 +555,7 @@ class IamDialogTest : AnnotationSpec() { val iamWebView = createWebView() iamWebView.webView = webView - whenever(mockWebViewFactory.create(null)).thenReturn(iamWebView) + whenever(mockWebViewFactory.create(mock())).thenReturn(iamWebView) val fragmentScenario = launchFragment { IamDialog( @@ -551,7 +580,7 @@ class IamDialogTest : AnnotationSpec() { val messageLoadedListener = MessageLoadedListener { } val mockIamWebView: IamWebView = mock() - whenever(mockWebViewFactory.create(null)).thenReturn(mockIamWebView) + whenever(mockWebViewFactory.create(mock())).thenReturn(mockIamWebView) val dialog = IamDialog( mobileEngage().timestampProvider, @@ -560,7 +589,7 @@ class IamDialogTest : AnnotationSpec() { ReflectionTestUtils.setInstanceField(dialog, "iamWebView", mockIamWebView) - dialog.loadInApp(html, inAppMetaData, messageLoadedListener) + dialog.loadInApp(html, inAppMetaData, messageLoadedListener, mock()) verify(mockIamWebView).load(html, inAppMetaData, messageLoadedListener) } diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt index 84e18d0b..1b82e0dd 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt @@ -1,10 +1,10 @@ package com.emarsys.mobileengage.iam.webview +import android.app.Activity import androidx.test.core.app.ActivityScenario import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.handler.ConcurrentHandlerHolder -import com.emarsys.core.provider.activity.CurrentActivityProvider import com.emarsys.mobileengage.iam.jsbridge.IamJsBridge import com.emarsys.mobileengage.iam.jsbridge.IamJsBridgeFactory import com.emarsys.mobileengage.iam.jsbridge.JSCommandFactory @@ -16,6 +16,7 @@ import io.kotest.matchers.shouldBe import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import java.lang.ref.WeakReference import java.util.concurrent.CountDownLatch class IamWebViewFactoryTest : AnnotationSpec() { @@ -25,11 +26,12 @@ class IamWebViewFactoryTest : AnnotationSpec() { private lateinit var mockJsBridgeFactory: IamJsBridgeFactory private lateinit var mockJsBridge: IamJsBridge private lateinit var concurrentHandlerHolder: ConcurrentHandlerHolder - private lateinit var mockCurrentActivityProvider: CurrentActivityProvider private lateinit var webViewFactory: IamWebViewFactory private lateinit var scenario: ActivityScenario + private lateinit var activityReference: WeakReference + @Before fun setUp() { mockJsBridge = mock() @@ -46,14 +48,11 @@ class IamWebViewFactoryTest : AnnotationSpec() { scenario = ActivityScenario.launch(FakeActivity::class.java) scenario.onActivity { activity -> - mockCurrentActivityProvider = mock { - on { get() } doReturn activity - } + activityReference = WeakReference(activity) webViewFactory = IamWebViewFactory( mockJsBridgeFactory, mockJSCommandFactoryProvider, - concurrentHandlerHolder, - mockCurrentActivityProvider + concurrentHandlerHolder ) } } @@ -66,7 +65,7 @@ class IamWebViewFactoryTest : AnnotationSpec() { @Test fun testCreateWithNull() { val iamWebView = runOnMain { - webViewFactory.create(null) + webViewFactory.create(activityReference.get()!!) } iamWebView::class.java shouldBe IamWebView::class.java } diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt index a7148c56..1dd35b43 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt @@ -3,6 +3,7 @@ package com.emarsys.mobileengage.responsehandler import android.content.ClipboardManager import androidx.test.core.app.ActivityScenario +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.handler.ConcurrentHandlerHolder import com.emarsys.core.provider.activity.CurrentActivityProvider @@ -34,7 +35,7 @@ class InAppMessageResponseHandlerTest : AnnotationSpec() { private lateinit var mockJsBridgeFactory: IamJsBridgeFactory private lateinit var mockClipboardManager: ClipboardManager private lateinit var mockJsBridge: IamJsBridge - private lateinit var mockCurrentActivityProvider: CurrentActivityProvider + private lateinit var mockCurrentActivityProvider: TransitionSafeCurrentActivityWatchdog private lateinit var scenario: ActivityScenario @Before @@ -43,7 +44,7 @@ class InAppMessageResponseHandlerTest : AnnotationSpec() { scenario.onActivity { activity -> concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() mockCurrentActivityProvider = mock { - on { get() } doReturn activity + on { activity() } doReturn activity } mockJsBridge = mock() mockClipboardManager = mock() diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt index af084f67..b0475083 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/OverlayInAppPresenter.kt @@ -1,8 +1,8 @@ package com.emarsys.mobileengage.iam import com.emarsys.core.Mockable +import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.handler.ConcurrentHandlerHolder -import com.emarsys.core.provider.activity.CurrentActivityProvider import com.emarsys.core.provider.activity.fragmentManager import com.emarsys.core.provider.timestamp.TimestampProvider import com.emarsys.core.util.log.Logger @@ -18,43 +18,47 @@ class OverlayInAppPresenter( private val concurrentHandlerHolder: ConcurrentHandlerHolder, private val dialogProvider: IamDialogProvider, private val timestampProvider: TimestampProvider, - private val currentActivityProvider: CurrentActivityProvider + private val currentActivityWatchdog: TransitionSafeCurrentActivityWatchdog ) { private var showingInProgress = false + fun present( campaignId: String, sid: String?, url: String?, requestId: String?, startTimestamp: Long, html: String, messageLoadedListener: MessageLoadedListener? ) { val shownDialog = - currentActivityProvider.get()?.fragmentManager()?.findFragmentByTag(IamDialog.TAG) + currentActivityWatchdog.activity().fragmentManager()?.findFragmentByTag(IamDialog.TAG) if (shownDialog == null && !showingInProgress) { showingInProgress = true concurrentHandlerHolder.postOnMain { try { val iamDialog = dialogProvider.provideDialog(campaignId, sid, url, requestId) - - iamDialog.loadInApp(html, InAppMetaData(campaignId, sid, url)) { - val activity = currentActivityProvider.get() - activity?.fragmentManager()?.let { - if (it.findFragmentByTag(IamDialog.TAG) == null) { - val endTimestamp = timestampProvider.provideTimestamp() - iamDialog.setInAppLoadingTime( - InAppLoadingTime( - startTimestamp, - endTimestamp + val activity = currentActivityWatchdog.activity() + iamDialog.loadInApp( + html, InAppMetaData(campaignId, sid, url), + { + activity.fragmentManager()?.let { + if (it.findFragmentByTag(IamDialog.TAG) == null) { + val endTimestamp = timestampProvider.provideTimestamp() + iamDialog.setInAppLoadingTime( + InAppLoadingTime( + startTimestamp, + endTimestamp + ) ) - ) - if (!it.isStateSaved) { - iamDialog.showNow(it, IamDialog.TAG) + if (!it.isStateSaved) { + iamDialog.showNow(it, IamDialog.TAG) + } } } - } - concurrentHandlerHolder.coreHandler.post { - messageLoadedListener?.onMessageLoaded() - showingInProgress = false - } - } + concurrentHandlerHolder.coreHandler.post { + messageLoadedListener?.onMessageLoaded() + showingInProgress = false + } + }, + activity + ) } catch (e: Exception) { concurrentHandlerHolder.coreHandler.post { Logger.error(CrashLog(e)) diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/dialog/IamDialog.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/dialog/IamDialog.kt index 15db2226..aa37e6bc 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/dialog/IamDialog.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/dialog/IamDialog.kt @@ -1,9 +1,11 @@ package com.emarsys.mobileengage.iam.dialog +import android.app.Activity import android.content.DialogInterface import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -25,6 +27,7 @@ import com.emarsys.mobileengage.iam.model.InAppMetaData import com.emarsys.mobileengage.iam.webview.IamWebView import com.emarsys.mobileengage.iam.webview.IamWebViewFactory import com.emarsys.mobileengage.iam.webview.MessageLoadedListener +import java.lang.ref.WeakReference @Mockable class IamDialog( @@ -50,15 +53,19 @@ class IamDialog( private var startTime: Long = 0 private var dismissed = false + private var activityReference: WeakReference? = null + private var iamWebView: IamWebView? = null fun loadInApp( html: String, inAppMetaData: InAppMetaData, - messageLoadedListener: MessageLoadedListener + messageLoadedListener: MessageLoadedListener, + activity: Activity ) { + this.activityReference = WeakReference(activity) if (iamWebView == null) { - iamWebView = webViewFactory.create(activity ?: context) + iamWebView = webViewFactory.create(activity) } this.iamWebView?.load(html, inAppMetaData, messageLoadedListener) } @@ -75,8 +82,9 @@ class IamDialog( super.onCreate(savedInstanceState) retainInstance = true setStyle(STYLE_NO_FRAME, android.R.style.Theme_Dialog) - if (iamWebView == null) { - iamWebView = webViewFactory.create(activity ?: context) + val activity = activityReference?.get() + if (iamWebView == null && activity != null) { + iamWebView = webViewFactory.create(activity) } } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactory.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactory.kt index c70cf130..3de0dc68 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactory.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactory.kt @@ -3,7 +3,6 @@ package com.emarsys.mobileengage.iam.webview import android.content.Context import com.emarsys.core.Mockable import com.emarsys.core.handler.ConcurrentHandlerHolder -import com.emarsys.core.provider.activity.CurrentActivityProvider import com.emarsys.mobileengage.iam.jsbridge.IamJsBridgeFactory import com.emarsys.mobileengage.iam.jsbridge.JSCommandFactoryProvider @@ -11,14 +10,13 @@ import com.emarsys.mobileengage.iam.jsbridge.JSCommandFactoryProvider class IamWebViewFactory( private val jsBridgeFactory: IamJsBridgeFactory, private val jsCommandFactoryProvider: JSCommandFactoryProvider, - private val concurrentHandlerHolder: ConcurrentHandlerHolder, - private val currentActivityProvider: CurrentActivityProvider + private val concurrentHandlerHolder: ConcurrentHandlerHolder ) { - fun create(context: Context?): IamWebView { + fun create(context: Context): IamWebView { return IamWebView( concurrentHandlerHolder, jsBridgeFactory, - jsCommandFactoryProvider.provide(), context ?: currentActivityProvider.get() + jsCommandFactoryProvider.provide(), context ) } From 5c8f958c4e69f5c1efe8f4d2895f7787d22a9f1f Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Wed, 31 Jul 2024 15:05:40 +0200 Subject: [PATCH 31/33] fix(emarsys): register predictShardTrigger even if predict is not enabled SUITEDEV-36062 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- .../src/androidTest/java/com/emarsys/EmarsysTest.kt | 7 +++---- emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt index d2196235..34bf9fd1 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt @@ -89,7 +89,6 @@ import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import java.util.concurrent.CountDownLatch @@ -442,14 +441,14 @@ class EmarsysTest : AnnotationSpec() { runBlockingOnCoreSdkThread { argumentCaptor().apply { - verify(mockCoreSQLiteDatabase, times(1)).registerTrigger( + verify(mockCoreSQLiteDatabase, times(2)).registerTrigger( any(), any(), any(), capture() ) - firstValue shouldBe mockLogShardTrigger - verifyNoMoreInteractions(mockCoreSQLiteDatabase) + firstValue shouldBe mockPredictShardTrigger + secondValue shouldBe mockLogShardTrigger } } } diff --git a/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt b/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt index acfa3760..38411a3d 100644 --- a/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt +++ b/emarsys-sdk/src/main/java/com/emarsys/Emarsys.kt @@ -196,7 +196,6 @@ object Emarsys { } private fun registerDatabaseTriggers() { - if (FeatureRegistry.isFeatureEnabled(PREDICT)) { emarsys().coreSQLiteDatabase .registerTrigger( DatabaseContract.SHARD_TABLE_NAME, @@ -204,7 +203,6 @@ object Emarsys { TriggerEvent.INSERT, emarsys().predictShardTrigger ) - } emarsys().coreSQLiteDatabase .registerTrigger( From 89f0ae9969e2f649b9808bea29465a53fe0d2c35 Mon Sep 17 00:00:00 2001 From: megamegax Date: Wed, 4 Sep 2024 10:30:06 +0200 Subject: [PATCH 32/33] test(mockk): convert failing tests to mockk SUITEDEV-36519 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> --- .../emarsys/core/util/AndroidVersionUtils.kt | 10 + .../emarsys/DefaultInboxIntegrationTest.kt | 19 +- .../java/com/emarsys/EmarsysTest.kt | 474 ++++++++++-------- .../InappNotificationIntegrationTest.kt | 63 ++- .../emarsys/MobileEngageIntegrationTest.kt | 27 +- ...ngageRefreshContactTokenIntegrationTest.kt | 30 +- .../com/emarsys/PredictIntegrationTest.kt | 20 +- .../emarsys/inapp/ui/InlineInAppViewTest.kt | 146 +++--- gradle/libs.versions.toml | 60 +-- .../deeplink/DeepLinkActionTest.kt | 48 +- .../device/DeviceInfoStartActionTest.kt | 31 +- .../mobileengage/iam/AppStartActionTest.kt | 35 +- .../iam/dialog/IamDialogProviderTest.kt | 25 +- .../mobileengage/iam/dialog/IamDialogTest.kt | 337 ++++++------- .../iam/jsbridge/IamJsBridgeTest.kt | 261 +++++----- .../iam/jsbridge/JSCommandFactoryTest.kt | 167 +++--- .../iam/webview/IamWebViewFactoryTest.kt | 60 +-- .../command/LaunchApplicationCommandTest.kt | 137 +++-- .../InAppMessageResponseHandlerTest.kt | 90 ++-- .../service/MessagingServiceUtilsTest.kt | 5 +- .../mobileengage/service/NotificationStyle.kt | 2 +- sample/build.gradle.kts | 1 + .../sample/dashboard/DashboardScreen.kt | 1 + 23 files changed, 999 insertions(+), 1050 deletions(-) diff --git a/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt b/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt index 912a4373..d1f71d58 100644 --- a/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt +++ b/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt @@ -1,6 +1,7 @@ package com.emarsys.core.util import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast object AndroidVersionUtils { val isOreoOrAbove: Boolean @@ -16,6 +17,15 @@ object AndroidVersionUtils { get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.S val isBelowTiramisu: Boolean get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + + val isTiramisuOrAbove: Boolean + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + + val isUOrAbove: Boolean + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + val isBelowUpsideDownCake: Boolean get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE } \ No newline at end of file diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/DefaultInboxIntegrationTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/DefaultInboxIntegrationTest.kt index 8e4e11be..08f7937b 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/DefaultInboxIntegrationTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/DefaultInboxIntegrationTest.kt @@ -15,15 +15,14 @@ import com.emarsys.testUtil.DatabaseTestUtils import com.emarsys.testUtil.InstrumentationRegistry import com.emarsys.testUtil.IntegrationTestUtils import com.emarsys.testUtil.KotestRunnerAndroid -import com.emarsys.testUtil.mockito.whenever import com.emarsys.testUtil.rules.ConnectionRule import com.emarsys.testUtil.rules.DuplicatedThreadRule import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockk import org.junit.Rule import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.mock import java.util.concurrent.CountDownLatch @@ -66,16 +65,16 @@ class DefaultInboxIntegrationTest : AnnotationSpec() { latch = CountDownLatch(1) val deviceInfo = DeviceInfo( application, - mock(HardwareIdProvider::class.java).apply { - whenever(provideHardwareId()).thenReturn("inboxv1_integration_hwid") + mockk(relaxed = true).apply { + every { provideHardwareId() } returns "inboxv1_integration_hwid" }, - mock(VersionProvider::class.java).apply { - whenever(provideSdkVersion()).thenReturn(SDK_VERSION) + mockk(relaxed = true).apply { + every { provideSdkVersion() } returns SDK_VERSION }, - mock(LanguageProvider::class.java).apply { - whenever(provideLanguage(any())).thenReturn(LANGUAGE) + mockk(relaxed = true).apply { + every { provideLanguage(any()) } returns LANGUAGE }, - mock(NotificationManagerHelper::class.java), + mockk(relaxed = true), isAutomaticPushSendingEnabled = true, isGooglePlayAvailable = true ) diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt index 34bf9fd1..d370b834 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/EmarsysTest.kt @@ -79,17 +79,15 @@ import com.emarsys.testUtil.rules.ConnectionRule import com.emarsys.testUtil.rules.DuplicatedThreadRule import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.Called +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import io.mockk.verifyOrder import org.junit.Rule -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.inOrder -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.spy -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever import java.util.concurrent.CountDownLatch class EmarsysTest : AnnotationSpec() { @@ -160,30 +158,30 @@ class EmarsysTest : AnnotationSpec() { @Before fun setUp() { - application = spy(getTargetContext().applicationContext as Application) - completionListener = mock() - mockResultListener = mock() - mockActivityLifecycleWatchdog = mock() - mockCurrentActivityWatchdog = mock() - mockCoreSQLiteDatabase = mock() - mockMobileEngageInternal = mock() - mockDeepLinkInternal = mock() - mockDeepLinkApi = mock() - mockLoggingDeepLinkApi = mock() - mockEventServiceInternal = mock() - mockEventServiceApi = mock() - mockLoggingEventServiceApi = mock() - mockClientServiceApi = mock() - mockPredictShardTrigger = mock() - mockLogShardTrigger = mock() - mockLanguageProvider = mock() - mockVersionProvider = mock() - inappEventHandler = mock() - mockDeviceInfoPayloadStorage = mock() - mockContactFieldValueStorage = mock() - mockContactTokenStorage = mock() - mockClientStateStorage = mock() - mockNotificationManagerHelper = mock() + application = spyk(getTargetContext().applicationContext as Application) + completionListener = mockk(relaxed = true) + mockResultListener = mockk(relaxed = true) + mockActivityLifecycleWatchdog = mockk(relaxed = true) + mockCurrentActivityWatchdog = mockk(relaxed = true) + mockCoreSQLiteDatabase = mockk(relaxed = true) + mockMobileEngageInternal = mockk(relaxed = true) + mockDeepLinkInternal = mockk(relaxed = true) + mockDeepLinkApi = mockk(relaxed = true) + mockLoggingDeepLinkApi = mockk(relaxed = true) + mockEventServiceInternal = mockk(relaxed = true) + mockEventServiceApi = mockk(relaxed = true) + mockLoggingEventServiceApi = mockk(relaxed = true) + mockClientServiceApi = mockk(relaxed = true) + mockPredictShardTrigger = mockk(relaxed = true) + mockLogShardTrigger = mockk(relaxed = true) + mockLanguageProvider = mockk(relaxed = true) + mockVersionProvider = mockk(relaxed = true) + inappEventHandler = mockk(relaxed = true) + mockDeviceInfoPayloadStorage = mockk(relaxed = true) + mockContactFieldValueStorage = mockk(relaxed = true) + mockContactTokenStorage = mockk(relaxed = true) + mockClientStateStorage = mockk(relaxed = true) + mockNotificationManagerHelper = mockk(relaxed = true) baseConfig = createConfig().build() mobileEngageConfig = createConfig() @@ -196,34 +194,35 @@ class EmarsysTest : AnnotationSpec() { .applicationCode(APPLICATION_CODE) .merchantId(MERCHANT_ID) .build() - mockRequestContext = mock() - mockHardwareIdProvider = mock() - mockMobileEngageApi = mock() - mockLoggingMobileEngageApi = mock() - mockInApp = mock() - mockLoggingInApp = mock() - mockPush = mock() - mockPredict = mock() - mockLoggingPredict = mock() - mockPredictRestricted = mock() - mockConfig = mock() - mockConfigInternal = mock() - mockMessageInbox = mock() - mockLogic = mock() - mockRecommendationFilter = mock() - predictResultListenerCallback = mock() - whenever(mockNotificationManagerHelper.channelSettings).thenReturn( - listOf( - ChannelSettings(channelId = "channelId") - ) - ) - whenever(mockNotificationManagerHelper.importance).thenReturn(-1000) - whenever(mockNotificationManagerHelper.areNotificationsEnabled).thenReturn(false) - whenever(mockHardwareIdProvider.provideHardwareId()).thenReturn("hwid") - whenever(mockLanguageProvider.provideLanguage(any())).thenReturn( - "language" - ) - whenever(mockVersionProvider.provideSdkVersion()).thenReturn("version") + mockRequestContext = mockk(relaxed = true) + mockHardwareIdProvider = mockk(relaxed = true) + mockMobileEngageApi = mockk(relaxed = true) + mockLoggingMobileEngageApi = mockk(relaxed = true) + mockInApp = mockk(relaxed = true) + mockLoggingInApp = mockk(relaxed = true) + mockPush = mockk(relaxed = true) + mockPredict = mockk(relaxed = true) + mockLoggingPredict = mockk(relaxed = true) + mockPredictRestricted = mockk(relaxed = true) + mockConfig = mockk(relaxed = true) + mockConfigInternal = mockk(relaxed = true) + mockMessageInbox = mockk(relaxed = true) + mockLogic = mockk(relaxed = true) + mockRecommendationFilter = mockk(relaxed = true) + predictResultListenerCallback = mockk(relaxed = true) + every { (mockNotificationManagerHelper.channelSettings) } returns + listOf( + ChannelSettings(channelId = "channelId") + ) + + every { mockNotificationManagerHelper.importance } returns -1000 + every { mockNotificationManagerHelper.areNotificationsEnabled } returns false + every { mockHardwareIdProvider.provideHardwareId() } returns "hwid" + every { + mockLanguageProvider.provideLanguage(any()) + } returns "language" + + every { mockVersionProvider.provideSdkVersion() } returns "version" deviceInfo = DeviceInfo( application, mockHardwareIdProvider, mockVersionProvider, @@ -231,13 +230,13 @@ class EmarsysTest : AnnotationSpec() { isAutomaticPushSendingEnabled = true, isGooglePlayAvailable = true ) - whenever(mockRequestContext.applicationCode).thenReturn(APPLICATION_CODE) - whenever(mockRequestContext.deviceInfo).thenReturn(deviceInfo) - whenever(mockVersionProvider.provideSdkVersion()).thenReturn(SDK_VERSION) - whenever(mockContactFieldValueStorage.get()).thenReturn("test@test.com") - whenever(mockContactTokenStorage.get()).thenReturn("contactToken") + every { mockRequestContext.applicationCode } returns APPLICATION_CODE + every { mockRequestContext.deviceInfo } returns deviceInfo + every { mockVersionProvider.provideSdkVersion() } returns SDK_VERSION + every { mockContactFieldValueStorage.get() } returns "test@test.com" + every { mockContactTokenStorage.get() } returns "contactToken" - whenever(mockDeviceInfoPayloadStorage.get()).thenReturn("deviceInfo.deviceInfoPayload") + every { mockDeviceInfoPayloadStorage.get() } returns "deviceInfo.deviceInfoPayload" setupEmarsysComponent( FakeDependencyContainer( @@ -274,7 +273,7 @@ class EmarsysTest : AnnotationSpec() { eventService = mockEventServiceApi, loggingEventService = mockLoggingEventServiceApi, deepLink = mockDeepLinkApi, - logger = mock() + logger = mockk(relaxed = true) ) ) latch = CountDownLatch(1) @@ -297,7 +296,7 @@ class EmarsysTest : AnnotationSpec() { FeatureRegistry.isFeatureEnabled(InnerFeature.MOBILE_ENGAGE) shouldBe false FeatureRegistry.isFeatureEnabled(InnerFeature.PREDICT) shouldBe false - verify(mockConfigInternal, times(0)).refreshRemoteConfig(any()) + verify(exactly = 0) { (mockConfigInternal).refreshRemoteConfig(any()) } } @Test @@ -314,7 +313,7 @@ class EmarsysTest : AnnotationSpec() { runBlockingOnCoreSdkThread() FeatureRegistry.isFeatureEnabled(InnerFeature.MOBILE_ENGAGE) shouldBe true - verify(mockConfigInternal).refreshRemoteConfig(any()) + verify { mockConfigInternal.refreshRemoteConfig(any()) } } @Test @@ -426,12 +425,14 @@ class EmarsysTest : AnnotationSpec() { setup(predictConfig) runBlockingOnCoreSdkThread { - verify(mockCoreSQLiteDatabase).registerTrigger( - "shard", - TriggerType.AFTER, - TriggerEvent.INSERT, - mockPredictShardTrigger - ) + verify { + mockCoreSQLiteDatabase.registerTrigger( + "shard", + TriggerType.AFTER, + TriggerEvent.INSERT, + mockPredictShardTrigger + ) + } } } @@ -440,16 +441,18 @@ class EmarsysTest : AnnotationSpec() { setup(mobileEngageConfig) runBlockingOnCoreSdkThread { - argumentCaptor().apply { - verify(mockCoreSQLiteDatabase, times(2)).registerTrigger( + val slots = mutableListOf() + verify(exactly = 2) { + mockCoreSQLiteDatabase.registerTrigger( any(), any(), any(), - capture() + capture(slots) ) - firstValue shouldBe mockPredictShardTrigger - secondValue shouldBe mockLogShardTrigger } + + slots[0] shouldBe mockPredictShardTrigger + slots[1] shouldBe mockLogShardTrigger } } @@ -458,12 +461,14 @@ class EmarsysTest : AnnotationSpec() { setup(mobileEngageConfig) runBlockingOnCoreSdkThread { - verify(mockCoreSQLiteDatabase).registerTrigger( - "shard", - TriggerType.AFTER, - TriggerEvent.INSERT, - mockLogShardTrigger - ) + verify { + mockCoreSQLiteDatabase.registerTrigger( + "shard", + TriggerType.AFTER, + TriggerEvent.INSERT, + mockLogShardTrigger + ) + } } } @@ -471,41 +476,39 @@ class EmarsysTest : AnnotationSpec() { fun testSetup_registers_activityLifecycleWatchdog() { setup(mobileEngageConfig) runBlockingOnCoreSdkThread { - argumentCaptor().apply { - verify( - application, - times(1), - ).registerActivityLifecycleCallbacks(capture()) - val allRegisteredWatchdogs = allValues - getElementByType( - allRegisteredWatchdogs, - ActivityLifecycleWatchdog::class.java - ) shouldNotBe null - } - argumentCaptor().apply { - verify( - application, - times(1), - ).registerActivityLifecycleCallbacks(capture()) - val allRegisteredWatchdogs = allValues - getElementByType( - allRegisteredWatchdogs, - CurrentActivityWatchdog::class.java - ) shouldNotBe null + val watchdogSlot = slot() + + verify(exactly = 1) { + application + .registerActivityLifecycleCallbacks(capture(watchdogSlot)) } + val allRegisteredWatchdogs = listOf(watchdogSlot.captured) + getElementByType( + allRegisteredWatchdogs, + ActivityLifecycleWatchdog::class.java + ) shouldNotBe null } - + val currentActivityWatchdogSlot = slot() + verify(exactly = 1) { + application + .registerActivityLifecycleCallbacks(capture(currentActivityWatchdogSlot)) + } + val allRegisteredWatchdogs = listOf(currentActivityWatchdogSlot.captured) + getElementByType( + allRegisteredWatchdogs, + CurrentActivityWatchdog::class.java + ) shouldNotBe null } + @Test fun testSetup_registers_activityLifecycleWatchdogs() { IntegrationTestUtils.tearDownEmarsys() - argumentCaptor().apply { - setup(mobileEngageConfig) + setup(mobileEngageConfig) + + runBlockingOnCoreSdkThread { + verify(exactly = 3) { application.registerActivityLifecycleCallbacks(any()) } - runBlockingOnCoreSdkThread { - verify(application, times(3)).registerActivityLifecycleCallbacks(capture()) - } } } @@ -529,9 +532,9 @@ class EmarsysTest : AnnotationSpec() { setup(mobileEngageConfig) runBlockingOnCoreSdkThread { - inOrder(application).apply { - verify(application).registerActivityLifecycleCallbacks(mockCurrentActivityWatchdog) - verify(application).registerActivityLifecycleCallbacks(mockActivityLifecycleWatchdog) + verifyOrder { + application.registerActivityLifecycleCallbacks(mockCurrentActivityWatchdog) + application.registerActivityLifecycleCallbacks(mockActivityLifecycleWatchdog) } } } @@ -541,108 +544,115 @@ class EmarsysTest : AnnotationSpec() { setup(mobileEngageConfig) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockInApp) + verify { + mockInApp wasNot Called + } } } @Test fun testSetup_sendClientInfo() { - whenever(mockClientStateStorage.get()).thenReturn(null) - whenever(mockContactFieldValueStorage.get()).thenReturn(null) - whenever(mockContactTokenStorage.get()).thenReturn(null) - whenever(mockRequestContext.hasContactIdentification()).thenReturn(false) - whenever(mockDeviceInfoPayloadStorage.get()).thenReturn("hardwareInfoPayload") + every { mockClientStateStorage.get() } returns null + every { mockContactFieldValueStorage.get() } returns null + every { mockContactTokenStorage.get() } returns null + every { mockRequestContext.hasContactIdentification() } returns false + every { mockDeviceInfoPayloadStorage.get() } returns "hardwareInfoPayload" setup(mobileEngageConfig) runBlockingOnCoreSdkThread() - verify(mockClientServiceApi).trackDeviceInfo(null) + verify { mockClientServiceApi.trackDeviceInfo(null) } } @Test fun testSetup_doNotSendClientInfo_whenHashIsUnChanged() { - whenever(mockContactTokenStorage.get()).thenReturn(null) - whenever(mockContactFieldValueStorage.get()).thenReturn(null) + every { mockContactTokenStorage.get() } returns null + every { mockContactFieldValueStorage.get() } returns null val expectedDeviceInfo = deviceInfo.deviceInfoPayload - whenever(mockClientStateStorage.get()).thenReturn("asdfsaf") - whenever(mockDeviceInfoPayloadStorage.get()).thenReturn(expectedDeviceInfo) + every { mockClientStateStorage.get() } returns "asdfsaf" + every { mockDeviceInfoPayloadStorage.get() } returns expectedDeviceInfo setup(mobileEngageConfig) - verify(mockClientServiceApi, never()).trackDeviceInfo(null) + verify(exactly = 0) { mockClientServiceApi.trackDeviceInfo(null) } } @Test fun testSetup_doNotSendClientInfo_whenAnonymousContactIsNotNeededToSend() { - whenever(mockClientStateStorage.get()).thenReturn(null) - whenever(mockContactFieldValueStorage.get()).thenReturn("asdf") - whenever(mockContactTokenStorage.get()).thenReturn("asdf") + every { mockClientStateStorage.get() } returns null + every { mockContactFieldValueStorage.get() } returns "asdf" + every { mockContactTokenStorage.get() } returns "asdf" setup(mobileEngageConfig) - verify(mockClientServiceApi, never()).trackDeviceInfo(null) + verify(exactly = 0) { mockClientServiceApi.trackDeviceInfo(null) } } @Test fun testSetup_sendAnonymousContact() { - whenever(mockContactTokenStorage.get()).thenReturn(null) - whenever(mockRequestContext.hasContactIdentification()).thenReturn(false) - + every { mockContactTokenStorage.get() } returns null + every { mockRequestContext.hasContactIdentification() } returns false + every { mockClientStateStorage.get() } returns null + every { mockContactFieldValueStorage.get() } returns null + every { mockDeviceInfoPayloadStorage.get() } returns "hardwareInfoPayload" setup(mobileEngageConfig) runBlockingOnCoreSdkThread() - verify(mockMobileEngageApi).setContact(null, null, null) + verify { mockMobileEngageApi.setContact(null, null, null) } } @Test fun testSetup_sendDeviceInfoAndAnonymousContact_inOrder() { - whenever(mockRequestContext.hasContactIdentification()).thenReturn(false) - whenever(mockContactTokenStorage.get()).thenReturn(null) + every { mockClientStateStorage.get() } returns null + every { mockContactFieldValueStorage.get() } returns null + every { mockContactTokenStorage.get() } returns null + every { mockRequestContext.hasContactIdentification() } returns false + every { mockDeviceInfoPayloadStorage.get() } returns "hardwareInfoPayload" setup(mobileEngageConfig) runBlockingOnCoreSdkThread() - val inOrder = inOrder(mockMobileEngageApi, mockClientServiceApi) - inOrder.verify(mockClientServiceApi).trackDeviceInfo(null) - inOrder.verify(mockMobileEngageApi).setContact(null, null, null) - inOrder.verifyNoMoreInteractions() - + verifyOrder { + mockClientServiceApi.trackDeviceInfo(null) + mockMobileEngageApi.setContact(null, null, null) + } + confirmVerified(mockMobileEngageApi, mockClientServiceApi) } @Test fun testSetup_doNotSendAnonymousContact_whenContactIsIdentified() { - whenever(mockContactTokenStorage.get()).thenReturn(null) - whenever(mockRequestContext.hasContactIdentification()).thenReturn(true) + every { mockContactTokenStorage.get() } returns null + every { mockRequestContext.hasContactIdentification() } returns true setup(mobileEngageConfig) - verify(mockMobileEngageApi, never()).setContact(null, null, null) + verify(exactly = 0) { mockMobileEngageApi.setContact(null, null, null) } } @Test fun testSetup_doNotSendAnonymousContact_whenContactTokenIsPresent() { - whenever(mockRequestContext.hasContactIdentification()).thenReturn(false) + every { mockRequestContext.hasContactIdentification() } returns false setup(mobileEngageConfig) - verify(mockMobileEngageApi, never()).setContact(null, null, null) + verify(exactly = 0) { mockMobileEngageApi.setContact(null, null, null) } } @Test fun testSetup_shouldNotCallTrackDeviceInfoAndSetContact_whenMobileEngageFeatureIsDisabled() { - whenever(mockContactFieldValueStorage.get()).thenReturn(null) - whenever(mockContactTokenStorage.get()).thenReturn(null) - whenever(mockClientStateStorage.get()).thenReturn(null) + every { mockContactFieldValueStorage.get() } returns null + every { mockContactTokenStorage.get() } returns null + every { mockClientStateStorage.get() } returns null setup(baseConfig) runBlockingOnCoreSdkThread() - verify(mockMobileEngageApi, never()).setContact(null, null, null) - verify(mockClientServiceApi, never()).trackDeviceInfo(null) + verify(exactly = 0) { mockMobileEngageApi.setContact(null, null, null) } + verify(exactly = 0) { mockClientServiceApi.trackDeviceInfo(null) } } @Test @@ -653,8 +663,10 @@ class EmarsysTest : AnnotationSpec() { runBlockingOnCoreSdkThread() runBlockingOnCoreSdkThread { - verify(mockPredictRestricted).setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE) - verifyNoInteractions(mockMobileEngageApi) + verify { mockPredictRestricted.setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE) } + verify { + mockMobileEngageApi wasNot Called + } } } @@ -665,12 +677,14 @@ class EmarsysTest : AnnotationSpec() { setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE, completionListener) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockPredictRestricted) - verify(mockMobileEngageApi).setContact( - CONTACT_FIELD_ID, - CONTACT_FIELD_VALUE, - completionListener - ) + verify { + mockPredictRestricted wasNot Called + mockMobileEngageApi.setContact( + CONTACT_FIELD_ID, + CONTACT_FIELD_VALUE, + completionListener + ) + } } } @@ -681,12 +695,14 @@ class EmarsysTest : AnnotationSpec() { setAuthenticatedContact(CONTACT_FIELD_ID, OPEN_ID_TOKEN, completionListener) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockPredictRestricted) - verify(mockMobileEngageApi).setAuthenticatedContact( - CONTACT_FIELD_ID, - OPEN_ID_TOKEN, - completionListener - ) + verify { + mockPredictRestricted wasNot Called + mockMobileEngageApi.setAuthenticatedContact( + CONTACT_FIELD_ID, + OPEN_ID_TOKEN, + completionListener + ) + } } } @@ -698,7 +714,7 @@ class EmarsysTest : AnnotationSpec() { runBlockingOnCoreSdkThread() - verifyNoInteractions(mockMobileEngageApi) + verify { mockMobileEngageApi wasNot Called } } @Test @@ -711,13 +727,15 @@ class EmarsysTest : AnnotationSpec() { runBlockingOnCoreSdkThread() runBlockingOnCoreSdkThread() - verify(mockMobileEngageApi).setAuthenticatedContact( - CONTACT_FIELD_ID, - OPEN_ID_TOKEN, - completionListener - ) + verify { + mockMobileEngageApi.setAuthenticatedContact( + CONTACT_FIELD_ID, + OPEN_ID_TOKEN, + completionListener + ) + } FeatureRegistry.isFeatureEnabled(InnerFeature.PREDICT) shouldBe false - verifyNoInteractions(mockPredictRestricted) + verify { mockPredictRestricted wasNot Called } } @Test @@ -727,7 +745,7 @@ class EmarsysTest : AnnotationSpec() { setAuthenticatedContact(CONTACT_FIELD_ID, OPEN_ID_TOKEN, completionListener) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockPredictRestricted) + verify { mockPredictRestricted wasNot Called } setAuthenticatedContact(CONTACT_FIELD_ID, OPEN_ID_TOKEN, completionListener) } } @@ -739,7 +757,7 @@ class EmarsysTest : AnnotationSpec() { setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE, completionListener) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockPredictRestricted) + verify { mockPredictRestricted wasNot Called } } } @@ -749,7 +767,7 @@ class EmarsysTest : AnnotationSpec() { setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE, completionListener) - verifyNoInteractions(mockMobileEngageApi) + verify { mockMobileEngageApi wasNot Called } } @Test @@ -759,12 +777,14 @@ class EmarsysTest : AnnotationSpec() { setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE, completionListener) runBlockingOnCoreSdkThread { - verify(mockPredictRestricted).setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE) - verify(mockMobileEngageApi).setContact( - CONTACT_FIELD_ID, - CONTACT_FIELD_VALUE, - completionListener - ) + verify { + mockPredictRestricted.setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE) + mockMobileEngageApi.setContact( + CONTACT_FIELD_ID, + CONTACT_FIELD_VALUE, + completionListener + ) + } } } @@ -775,12 +795,14 @@ class EmarsysTest : AnnotationSpec() { setContact(CONTACT_FIELD_ID, CONTACT_FIELD_VALUE, completionListener) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockPredictRestricted) - verify(mockLoggingMobileEngageApi).setContact( - CONTACT_FIELD_ID, - CONTACT_FIELD_VALUE, - completionListener - ) + verify { + mockPredictRestricted wasNot Called + mockLoggingMobileEngageApi.setContact( + CONTACT_FIELD_ID, + CONTACT_FIELD_VALUE, + completionListener + ) + } } } @@ -792,8 +814,10 @@ class EmarsysTest : AnnotationSpec() { runBlockingOnCoreSdkThread() runBlockingOnCoreSdkThread { - verifyNoInteractions(mockMobileEngageApi) - verify(mockPredictRestricted).clearContact() + verify { + mockMobileEngageApi wasNot Called + mockPredictRestricted.clearContact() + } } } @@ -804,8 +828,10 @@ class EmarsysTest : AnnotationSpec() { clearContact(completionListener) runBlockingOnCoreSdkThread { - verify(mockMobileEngageApi).clearContact(completionListener) - verifyNoInteractions(mockPredictRestricted) + verify { + mockMobileEngageApi.clearContact(completionListener) + mockPredictRestricted wasNot Called + } } } @@ -815,7 +841,7 @@ class EmarsysTest : AnnotationSpec() { clearContact(completionListener) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockPredictRestricted) + verify { mockPredictRestricted wasNot Called } } } @@ -825,7 +851,7 @@ class EmarsysTest : AnnotationSpec() { clearContact(completionListener) - verifyNoInteractions(mockMobileEngageApi) + verify { mockMobileEngageApi wasNot Called } } @Test @@ -835,10 +861,10 @@ class EmarsysTest : AnnotationSpec() { clearContact(completionListener) runBlockingOnCoreSdkThread { - verify(mockPredictRestricted).clearContact() + verify { mockPredictRestricted.clearContact() } } - verify(mockMobileEngageApi).clearContact(completionListener) + verify { mockMobileEngageApi.clearContact(completionListener) } } @Test @@ -847,20 +873,20 @@ class EmarsysTest : AnnotationSpec() { clearContact(completionListener) runBlockingOnCoreSdkThread { - verifyNoInteractions(mockPredictRestricted) + verify { mockPredictRestricted wasNot Called } } - verify(mockLoggingMobileEngageApi).clearContact(completionListener) + verify { mockLoggingMobileEngageApi.clearContact(completionListener) } } @Test fun testTrackDeepLink_delegatesTo_deepLinkApi() { setup(createConfig().applicationCode(APPLICATION_CODE).build()) - val mockActivity: Activity = mock() - val mockIntent: Intent = mock() + val mockActivity: Activity = mockk(relaxed = true) + val mockIntent: Intent = mockk(relaxed = true) trackDeepLink(mockActivity, mockIntent) runBlockingOnCoreSdkThread { - verify(mockDeepLinkApi).trackDeepLinkOpen(mockActivity, mockIntent, null) + verify { mockDeepLinkApi.trackDeepLinkOpen(mockActivity, mockIntent, null) } } } @@ -872,11 +898,13 @@ class EmarsysTest : AnnotationSpec() { trackCustomEvent(eventName, eventAttributes, completionListener) runBlockingOnCoreSdkThread { - verify(mockEventServiceApi).trackCustomEventAsync( - eventName, - eventAttributes, - completionListener - ) + verify { + mockEventServiceApi.trackCustomEventAsync( + eventName, + eventAttributes, + completionListener + ) + } } } @@ -887,11 +915,13 @@ class EmarsysTest : AnnotationSpec() { trackCustomEvent(eventName, eventAttributes, completionListener) runBlockingOnCoreSdkThread() - verify(mockLoggingEventServiceApi).trackCustomEventAsync( - eventName, - eventAttributes, - completionListener - ) + verify { + mockLoggingEventServiceApi.trackCustomEventAsync( + eventName, + eventAttributes, + completionListener + ) + } } @Test @@ -900,8 +930,10 @@ class EmarsysTest : AnnotationSpec() { FeatureRegistry.enableFeature(InnerFeature.MOBILE_ENGAGE) Emarsys.inApp.isPaused - verify(mockInApp).isPaused - verifyNoInteractions(mockLoggingInApp) + verify { + mockInApp.isPaused + mockLoggingInApp wasNot Called + } } @Test @@ -910,8 +942,11 @@ class EmarsysTest : AnnotationSpec() { FeatureRegistry.enableFeature(InnerFeature.PREDICT) Emarsys.predict.trackItemView("testItemId") - verify(mockPredict).trackItemView("testItemId") - verifyNoInteractions(mockLoggingPredict) + verify { + mockPredict.trackItemView("testItemId") + mockLoggingPredict wasNot Called + } + } private fun createConfig(vararg experimentalFeatures: FlipperFeature): EmarsysConfig.Builder { @@ -929,7 +964,7 @@ class EmarsysTest : AnnotationSpec() { runBlockingOnCoreSdkThread() - verify(mockConfigInternal, times(0)).refreshRemoteConfig(any()) + verify(exactly = 0) { mockConfigInternal.refreshRemoteConfig(any()) } } private fun runBlockingOnCoreSdkThread(callback: (() -> Unit)? = null) { @@ -948,6 +983,5 @@ class EmarsysTest : AnnotationSpec() { if (exception != null) { throw exception as Exception } - } } \ No newline at end of file diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt index a8d0422c..0bf89c86 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/InappNotificationIntegrationTest.kt @@ -23,11 +23,9 @@ import com.emarsys.testUtil.DatabaseTestUtils import com.emarsys.testUtil.InstrumentationRegistry import com.emarsys.testUtil.IntegrationTestUtils import com.emarsys.testUtil.TestUrls.LARGE_IMAGE -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import java.util.concurrent.CountDownLatch class InappNotificationIntegrationTest : AnnotationSpec() { @@ -62,38 +60,38 @@ class InappNotificationIntegrationTest : AnnotationSpec() { .applicationCode(APP_ID) .build() - mockInappPresenterOverlay = mock() + mockInappPresenterOverlay = mockk(relaxed = true) - whenever( + every { mockInappPresenterOverlay.present( - anyOrNull(), - eq(SID), - anyOrNull(), - eq(null), - anyOrNull(), - anyOrNull(), - eq(null) + any(), + SID, + any(), + null, + any(), + any(), + null ) - ).thenAnswer { + } answers { completionListenerLatch.countDown() } - mockCurrentActivityProvider = mock() - val mockActivity = mock() - whenever(mockCurrentActivityProvider.get()).thenReturn(mockActivity) + mockCurrentActivityProvider = mockk(relaxed = true) + val mockActivity = mockk(relaxed = true) + every { mockCurrentActivityProvider.get() } returns mockActivity DefaultEmarsysDependencies(baseConfig, object : DefaultEmarsysComponent(baseConfig) { override val overlayInAppPresenter: OverlayInAppPresenter get() = mockInappPresenterOverlay override val activityLifecycleWatchdog: ActivityLifecycleWatchdog - get() = mock() + get() = mockk(relaxed = true) override val currentActivityProvider: CurrentActivityProvider get() = mockCurrentActivityProvider override val activityLifecycleActionRegistry: ActivityLifecycleActionRegistry - get() = mock() + get() = mockk(relaxed = true) override val appLifecycleObserver: AppLifecycleObserver - get() = mock() + get() = mockk(relaxed = true) override val eventServiceInternal: EventServiceInternal - get() = mock() + get() = mockk(relaxed = true) }) ConnectionTestUtils.checkConnection(application) @@ -143,15 +141,16 @@ class InappNotificationIntegrationTest : AnnotationSpec() { application.startActivity(intent) completionListenerLatch.await() - verify(mockInappPresenterOverlay).present( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) + verify { + mockInappPresenterOverlay.present( + any(), + any(), + any(), + any(), + any(), + any(), + any() + ) + } } - } \ No newline at end of file diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageIntegrationTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageIntegrationTest.kt index 11e8efb1..3ed6c427 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageIntegrationTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageIntegrationTest.kt @@ -25,15 +25,14 @@ import com.emarsys.testUtil.DatabaseTestUtils import com.emarsys.testUtil.InstrumentationRegistry import com.emarsys.testUtil.IntegrationTestUtils import com.emarsys.testUtil.RetryUtils -import com.emarsys.testUtil.mockito.whenever import com.emarsys.testUtil.rules.DuplicatedThreadRule import com.emarsys.testUtil.rules.RetryRule import io.kotest.matchers.ints.shouldBeInRange import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockk import org.junit.Rule -import org.mockito.ArgumentMatchers -import org.mockito.Mockito.mock import java.util.concurrent.CountDownLatch class MobileEngageIntegrationTest : AnnotationSpec() { @@ -81,21 +80,21 @@ class MobileEngageIntegrationTest : AnnotationSpec() { completionHandler = createDefaultCoreCompletionHandler() - val mockPushTokenProvider = mock(PushTokenProvider::class.java).apply { - whenever(providePushToken()).thenReturn("integration_test_push_token") + val mockPushTokenProvider = mockk(relaxed = true).apply { + every { providePushToken() } returns "integration_test_push_token" } val deviceInfo = DeviceInfo( application, - mock(HardwareIdProvider::class.java).apply { - whenever(provideHardwareId()).thenReturn("mobileengage_integration_hwid") + mockk(relaxed = true).apply { + every { provideHardwareId() } returns "mobileengage_integration_hwid" }, - mock(VersionProvider::class.java).apply { - whenever(provideSdkVersion()).thenReturn("0.0.0-mobileengage_integration_version") + mockk(relaxed = true).apply { + every { provideSdkVersion() } returns "0.0.0-mobileengage_integration_version" }, - mock(LanguageProvider::class.java).apply { - whenever(provideLanguage(ArgumentMatchers.any())).thenReturn("en-US") + mockk(relaxed = true).apply { + every { provideLanguage(any()) } returns "en-US" }, - mock(NotificationManagerHelper::class.java), + mockk(relaxed = true), isAutomaticPushSendingEnabled = true, isGooglePlayAvailable = true ) @@ -219,8 +218,8 @@ class MobileEngageIntegrationTest : AnnotationSpec() { fun testDeepLinkOpen() { Thread.sleep(1000) - val activity = mock(Activity::class.java) - whenever(activity.intent).thenReturn(Intent()) + val activity = mockk(relaxed = true) + every { activity.intent } returns Intent() val intent = Intent( Intent.ACTION_VIEW, diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageRefreshContactTokenIntegrationTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageRefreshContactTokenIntegrationTest.kt index b0afc03b..39c49d8f 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageRefreshContactTokenIntegrationTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/MobileEngageRefreshContactTokenIntegrationTest.kt @@ -21,14 +21,13 @@ import com.emarsys.testUtil.FeatureTestUtils import com.emarsys.testUtil.InstrumentationRegistry import com.emarsys.testUtil.IntegrationTestUtils import com.emarsys.testUtil.RetryUtils -import com.emarsys.testUtil.mockito.whenever import com.emarsys.testUtil.rules.DuplicatedThreadRule import com.emarsys.testUtil.rules.RetryRule import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockk import org.junit.Rule -import org.mockito.ArgumentMatchers -import org.mockito.Mockito import java.util.concurrent.CountDownLatch class MobileEngageRefreshContactTokenIntegrationTest : AnnotationSpec() { @@ -68,16 +67,16 @@ class MobileEngageRefreshContactTokenIntegrationTest : AnnotationSpec() { val deviceInfo = DeviceInfo( application, - Mockito.mock(HardwareIdProvider::class.java).apply { - whenever(provideHardwareId()).thenReturn("mobileengage_integration_hwid") + mockk(relaxed = true).apply { + every { provideHardwareId() } returns "mobileengage_integration_hwid" }, - Mockito.mock(VersionProvider::class.java).apply { - whenever(provideSdkVersion()).thenReturn("0.0.0-mobileengage_integration_version") + mockk(relaxed = true).apply { + every { provideSdkVersion() } returns "0.0.0-mobileengage_integration_version" }, - Mockito.mock(LanguageProvider::class.java).apply { - whenever(provideLanguage(ArgumentMatchers.any())).thenReturn("en-US") + mockk(relaxed = true).apply { + every { provideLanguage(any()) } returns "en-US" }, - Mockito.mock(NotificationManagerHelper::class.java), + mockk(relaxed = true), isAutomaticPushSendingEnabled = true, isGooglePlayAvailable = true ) @@ -91,7 +90,10 @@ class MobileEngageRefreshContactTokenIntegrationTest : AnnotationSpec() { ConnectionTestUtils.checkConnection(application) - sharedPreferences = application.getSharedPreferences("emarsys_secure_shared_preferences", Context.MODE_PRIVATE) + sharedPreferences = application.getSharedPreferences( + "emarsys_secure_shared_preferences", + Context.MODE_PRIVATE + ) Emarsys.setup(baseConfig) @@ -119,7 +121,11 @@ class MobileEngageRefreshContactTokenIntegrationTest : AnnotationSpec() { val eventServiceInternal = emarsys().eventServiceInternal - eventServiceInternal.trackInternalCustomEvent("integrationTest", emptyMap(), this::eventuallyStoreResult).apply { eventuallyAssertSuccess() } + eventServiceInternal.trackInternalCustomEvent( + "integrationTest", + emptyMap(), + this::eventuallyStoreResult + ).apply { eventuallyAssertSuccess() } contactTokenStorage.get() shouldNotBe "tokenForIntegrationTest" } diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt index 6ed0ff22..94364607 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/PredictIntegrationTest.kt @@ -27,15 +27,15 @@ import com.emarsys.testUtil.DatabaseTestUtils import com.emarsys.testUtil.FeatureTestUtils import com.emarsys.testUtil.InstrumentationRegistry import com.emarsys.testUtil.IntegrationTestUtils -import com.emarsys.testUtil.mockito.whenever import com.emarsys.testUtil.rules.ConnectionRule import com.emarsys.testUtil.rules.DuplicatedThreadRule import io.kotest.matchers.comparables.shouldBeGreaterThan import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockk import org.junit.Rule -import org.mockito.ArgumentMatchers -import org.mockito.Mockito.mock + import java.net.URLDecoder import java.util.concurrent.CountDownLatch import kotlin.reflect.KMutableProperty0 @@ -124,16 +124,16 @@ class PredictIntegrationTest : AnnotationSpec() { val deviceInfo = DeviceInfo( application, - mock(HardwareIdProvider::class.java).apply { - whenever(provideHardwareId()).thenReturn("mobileengage_integration_hwid") + mockk(relaxed = true).apply { + every { provideHardwareId() } returns "mobileengage_integration_hwid" }, - mock(VersionProvider::class.java).apply { - whenever(provideSdkVersion()).thenReturn("0.0.0-mobileengage_integration_version") + mockk(relaxed = true).apply { + every { provideSdkVersion() } returns "0.0.0-mobileengage_integration_version" }, - mock(LanguageProvider::class.java).apply { - whenever(provideLanguage(ArgumentMatchers.any())).thenReturn("en-US") + mockk(relaxed = true).apply { + every { provideLanguage(any()) } returns "en-US" }, - mock(NotificationManagerHelper::class.java), + mockk(relaxed = true), isAutomaticPushSendingEnabled = true, isGooglePlayAvailable = true ) diff --git a/emarsys-sdk/src/androidTest/java/com/emarsys/inapp/ui/InlineInAppViewTest.kt b/emarsys-sdk/src/androidTest/java/com/emarsys/inapp/ui/InlineInAppViewTest.kt index 51329d0d..f312fa6e 100644 --- a/emarsys-sdk/src/androidTest/java/com/emarsys/inapp/ui/InlineInAppViewTest.kt +++ b/emarsys-sdk/src/androidTest/java/com/emarsys/inapp/ui/InlineInAppViewTest.kt @@ -9,6 +9,7 @@ import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.handler.ConcurrentHandlerHolder import com.emarsys.core.request.RequestManager import com.emarsys.core.request.model.RequestModel + import com.emarsys.core.response.ResponseModel import com.emarsys.di.FakeDependencyContainer import com.emarsys.di.setupEmarsysComponent @@ -27,15 +28,11 @@ import com.emarsys.testUtil.IntegrationTestUtils import com.emarsys.testUtil.fake.FakeActivity import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify import org.json.JSONObject -import org.mockito.Mockito.spy -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever import java.util.concurrent.CountDownLatch class InlineInAppViewTest : AnnotationSpec() { @@ -63,12 +60,13 @@ class InlineInAppViewTest : AnnotationSpec() { fun setUp() { concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() - mockJsBridge = mock() - mockIamJsBridgeFactory = mock { on { createJsBridge(any()) } doReturn mockJsBridge } - mockJsCommandFactory = mock() + mockJsBridge = mockk(relaxed = true) + mockIamJsBridgeFactory = + mockk(relaxed = true) { every { createJsBridge(any()) } returns mockJsBridge } + mockJsCommandFactory = mockk(relaxed = true) scenario = ActivityScenario.launch(FakeActivity::class.java) scenario.onActivity { activity -> - mockIamWebView = spy(runOnMain { + mockIamWebView = spyk(runOnMain { IamWebView( concurrentHandlerHolder, mockIamJsBridgeFactory, @@ -77,18 +75,20 @@ class InlineInAppViewTest : AnnotationSpec() { ) }) - mockWebViewFactory = mock { - on { create(activity) }.thenReturn(mockIamWebView) + mockWebViewFactory = mockk(relaxed = true) { + every { create(activity) } returns (mockIamWebView) } - mockRequestModel = mock { - on { id } doReturn REQUEST_ID + mockRequestModel = mockk(relaxed = true) { + every { id } returns REQUEST_ID } - mockResponseModel = mock { - on { requestModel } doReturn mockRequestModel + mockResponseModel = mockk(relaxed = true) { + every { requestModel } returns mockRequestModel } - mockRequestManager = mock() - mockRequestModelFactory = mock { - on { createFetchInlineInAppMessagesRequest("testViewId") }.doReturn(mockRequestModel) + mockRequestManager = mockk(relaxed = true) + mockRequestModelFactory = mockk(relaxed = true) { + every { createFetchInlineInAppMessagesRequest("testViewId") }.returns( + mockRequestModel + ) } setupEmarsysComponent( @@ -127,11 +127,13 @@ class InlineInAppViewTest : AnnotationSpec() { val expectedHtml = "Hello World" - verify(mockIamWebView).load( - eq(expectedHtml), - eq(InAppMetaData("765", null, null)), - any() - ) + verify { + mockIamWebView.load( + (expectedHtml), + (InAppMetaData("765", null, null)), + any() + ) + } } @Test @@ -148,11 +150,13 @@ class InlineInAppViewTest : AnnotationSpec() { inlineInAppView.loadInApp(VIEW_ID) latch.await() - verify(mockIamWebView, times(0)).load( - any(), - any(), - any() - ) + verify(exactly = 0) { + mockIamWebView.load( + any(), + any(), + any() + ) + } } @Test @@ -173,11 +177,13 @@ class InlineInAppViewTest : AnnotationSpec() { error shouldNotBe null error!!.message shouldBe "Inline In-App HTML content must not be empty, please check your viewId!" - verify(mockIamWebView, times(0)).load( - any(), - any(), - any() - ) + verify(exactly = 0) { + mockIamWebView.load( + any(), + any(), + any() + ) + } } @Test @@ -190,11 +196,13 @@ class InlineInAppViewTest : AnnotationSpec() { inlineInAppView.loadInApp(VIEW_ID) latch.await() - verify(mockIamWebView, times(0)).load( - any(), - any(), - any() - ) + verify(exactly = 0) { + mockIamWebView.load( + any(), + any(), + any() + ) + } } @Test @@ -203,9 +211,9 @@ class InlineInAppViewTest : AnnotationSpec() { val expectedStatusCode = 500 val expectedMessage = "Error message" requestManagerRespond(false, responseBody = JSONObject()) - whenever(mockResponseModel.body).thenReturn(expectedBody) - whenever(mockResponseModel.statusCode).thenReturn(expectedStatusCode) - whenever(mockResponseModel.message).thenReturn(expectedMessage) + every { mockResponseModel.body } returns expectedBody + every { mockResponseModel.statusCode } returns expectedStatusCode + every { mockResponseModel.message } returns expectedMessage var error: Throwable? = null @@ -222,11 +230,13 @@ class InlineInAppViewTest : AnnotationSpec() { (error as ResponseErrorException).statusCode shouldBe expectedStatusCode (error as ResponseErrorException).statusMessage shouldBe expectedMessage - verify(mockIamWebView, times(0)).load( - any(), - any(), - any() - ) + verify(exactly = 0) { + mockIamWebView.load( + any(), + any(), + any() + ) + } } @Test @@ -242,11 +252,13 @@ class InlineInAppViewTest : AnnotationSpec() { inlineInAppView.loadInApp(VIEW_ID) latch.await() - verify(mockIamWebView, times(0)).load( - any(), - any(), - any() - ) + verify(exactly = 0) { + mockIamWebView.load( + any(), + any(), + any() + ) + } } @Test @@ -257,7 +269,7 @@ class InlineInAppViewTest : AnnotationSpec() { |{"campaignId":"7625","html":"Hello World2","viewId":"$OTHER_VIEW_ID"}],"oldCampaigns":[]}""".trimMargin() ) requestManagerRespond(responseBody = expectedBody) - val mockOnCloseListener: OnCloseListener = mock() + val mockOnCloseListener: OnCloseListener = mockk(relaxed = true) val latch = CountDownLatch(1) inlineInAppView.onCloseListener = mockOnCloseListener @@ -267,9 +279,12 @@ class InlineInAppViewTest : AnnotationSpec() { inlineInAppView.loadInApp(VIEW_ID) latch.await() - whenever(mockIamWebView.onCloseTriggered).thenAnswer { - val closeListener = it.arguments[0] + every { + mockIamWebView.onCloseTriggered + } answers { + val closeListener = it.invocation.args[0] closeListener shouldBe mockOnCloseListener + null } } @@ -282,7 +297,7 @@ class InlineInAppViewTest : AnnotationSpec() { |{"campaignId":"7625","html":"Hello World2","viewId":"$OTHER_VIEW_ID"}],"oldCampaigns":[]}""".trimMargin() ) requestManagerRespond(responseBody = expectedBody) - val mockAppEventListener: OnAppEventListener = mock() + val mockAppEventListener: OnAppEventListener = mockk(relaxed = true) val latch = CountDownLatch(1) inlineInAppView.onAppEventListener = mockAppEventListener @@ -292,9 +307,12 @@ class InlineInAppViewTest : AnnotationSpec() { inlineInAppView.loadInApp(VIEW_ID) latch.await() - whenever(mockIamWebView.onAppEventTriggered).thenAnswer { - val appEventListener = it.arguments[0] + every { + mockIamWebView.onAppEventTriggered + } answers { + val appEventListener = it.invocation.args[0] appEventListener shouldBe mockAppEventListener + null } } @@ -303,11 +321,13 @@ class InlineInAppViewTest : AnnotationSpec() { responseBody: JSONObject? = null, exception: Exception? = null ) { - whenever(mockRequestManager.submitNow(any(), any())).thenAnswer { + every { + mockRequestManager.submitNow(any(), any()) + } answers { val coreCompletionHandler: CoreCompletionHandler = - (it.arguments[1] as CoreCompletionHandler) + (it.invocation.args[1] as CoreCompletionHandler) if (responseBody != null) { - whenever(mockResponseModel.parsedBody).thenReturn(responseBody) + every { mockResponseModel.parsedBody } returns responseBody if (success) { coreCompletionHandler.onSuccess(REQUEST_ID, mockResponseModel) } else { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63176e5e..b38999cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,58 +1,58 @@ [versions] agp = "8.2.2" -androidx-test-core = "1.4.0" -kotlin = "1.9.21" -kotlinxCoroutines = "1.7.3" -compose = "1.6.1" -androidx-navigation-compose = "2.7.6" -compose-compiler = "1.5.6" +androidx-test-core = "1.6.1" +kotlin = "1.9.25" +kotlinxCoroutines = "1.8.1" +compose = "1.6.8" +androidx-navigation-compose = "2.7.7" +compose-compiler = "1.5.15" compose-plugin = "1.6.0-dev1419" android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" -androidx-activity = "1.8.2" -androidx-activityCompose = "1.8.2" -androidx-annotation = "1.7.1" -androidx-appcompat = "1.6.1" +androidx-activity = "1.9.1" +androidx-activityCompose = "1.9.1" +androidx-annotation = "1.8.2" +androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.1.4" -androidx-core-ktx = "1.12.0" -androidx-datastore-preferences = "1.0.0" -androidx-espresso-core = "3.5.1" -androidx-material = "1.11.0" -androidx-test-runner = "1.5.2" -androidx-test-rules = "1.5.0" -androidx-test-junit = "1.1.5" -androidx-test-fragment = "1.6.2" +androidx-core-ktx = "1.13.1" +androidx-datastore-preferences = "1.1.1" +androidx-espresso-core = "3.6.1" +androidx-material = "1.12.0" +androidx-test-runner = "1.6.2" +androidx-test-rules = "1.6.1" +androidx-test-junit = "1.2.1" +androidx-test-fragment = "1.8.2" androidx-test-multidex = "2.0.0" -androidx-lifecycle = "2.7.0" -androidx-espresso-idling-resource = "3.5.1" +androidx-lifecycle = "2.8.4" +androidx-espresso-idling-resource = "3.6.1" androidx-security-crypto = "1.1.0-alpha06" -androidx-navigation-safe-args = "2.5.0" +androidx-navigation-safe-args = "2.7.7" io-coil = "2.5.0" dokka = "1.9.10" junit = "4.13.2" -playServicesAuth = "20.7.0" +playServicesAuth = "21.2.0" tink = "1.9.0" kotpref = "2.13.0" kotest = "5.8.0" -mockk = "1.13.9" -fcm = "23.4.0" -firebase-common = "20.4.2" +mockk = "1.13.10" +fcm = "24.0.1" +firebase-common = "21.0.0" google-gson = "2.10.1" agconnect_core = "1.9.1.300" -hms_push = "6.11.0.300" -googleServices = "4.4.1" -google-play-services-location = "21.1.0" +hms_push = "6.12.0.300" +googleServices = "4.4.2" +google-play-services-location = "21.3.0" google-play-services-auth = "20.7.0" google-accompanist-swipetorefresh = "0.34.0" -webkit = "1.7.0" +webkit = "1.11.0" mockito-android = "5.10.0" mockito-core = "5.10.0" mockito-kotlin = "4.1.0" grGit = "5.2.2" byte-buddy = "1.14.11" dotEnv = "4.0.0" -android-tools-desugar = "2.0.4" +android-tools-desugar = "2.1.1" benManesVersions = "0.51.0" android-junit5 = "1.10.0.0" nexus-publish = "1.3.0" diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/deeplink/DeepLinkActionTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/deeplink/DeepLinkActionTest.kt index f3abf13b..5ad45901 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/deeplink/DeepLinkActionTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/deeplink/DeepLinkActionTest.kt @@ -9,21 +9,12 @@ import com.emarsys.mobileengage.util.waitForTask import com.emarsys.testUtil.AnnotationSpec -import org.mockito.Mockito -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify class DeepLinkActionTest : AnnotationSpec() { - companion object { - init { - Mockito.mock(Intent::class.java) - Mockito.mock(Activity::class.java) - } - } - - private lateinit var deepLinkInternal: DeepLinkInternal + private lateinit var mockDeepLinkInternal: DeepLinkInternal private lateinit var action: DeepLinkAction @@ -32,8 +23,8 @@ class DeepLinkActionTest : AnnotationSpec() { setupMobileEngageComponent(FakeMobileEngageDependencyContainer()) - deepLinkInternal = mock() - action = DeepLinkAction(deepLinkInternal) + mockDeepLinkInternal = mockk(relaxed = true) + action = DeepLinkAction(mockDeepLinkInternal) } @After @@ -43,23 +34,30 @@ class DeepLinkActionTest : AnnotationSpec() { @Test fun testExecute_callsMobileEngageInternal() { - val intent: Intent = mock() - val activity: Activity = mock() - whenever(activity.intent).thenReturn(intent) + val intent: Intent = mockk(relaxed = true) + val mockActivity: Activity = mockk(relaxed = true) + every { mockActivity.intent } returns intent - action.execute(activity) - waitForTask() + action.execute(mockActivity) - verify(deepLinkInternal).trackDeepLinkOpen(activity, intent, null) + verify { mockDeepLinkInternal.trackDeepLinkOpen(mockActivity, intent, null) } } @Test fun testExecute_neverCallsMobileEngageInternal_whenIntentFromActivityIsNull() { - val activity: Activity = mock() - action.execute(activity) + val mockActivity: Activity = mockk(relaxed = true) + every { mockActivity.intent } returns null + + action.execute(mockActivity) waitForTask() - verifyNoInteractions(deepLinkInternal) + verify(exactly = 0) { + (mockDeepLinkInternal.trackDeepLinkOpen( + eq(mockActivity), + any(), + any() + )) + } } @Test @@ -67,6 +65,6 @@ class DeepLinkActionTest : AnnotationSpec() { action.execute(null) waitForTask() - verifyNoInteractions(deepLinkInternal) + verify(exactly = 0) { (mockDeepLinkInternal.trackDeepLinkOpen(any(), any(), any())) } } } \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/device/DeviceInfoStartActionTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/device/DeviceInfoStartActionTest.kt index 88b4c545..0ef536b9 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/device/DeviceInfoStartActionTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/device/DeviceInfoStartActionTest.kt @@ -10,10 +10,11 @@ import com.emarsys.mobileengage.fake.FakeMobileEngageDependencyContainer import com.emarsys.mobileengage.util.waitForTask import com.emarsys.testUtil.AnnotationSpec import com.emarsys.testUtil.SharedPrefsUtils -import com.emarsys.testUtil.mockito.whenever -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions +import io.mockk.every + +import io.mockk.mockk +import io.mockk.verify + class DeviceInfoStartActionTest : AnnotationSpec() { @@ -26,9 +27,9 @@ class DeviceInfoStartActionTest : AnnotationSpec() { @Before @Suppress("UNCHECKED_CAST") fun setUp() { - deviceInfoPayloadStorage = mock() - mockClientServiceInternal = mock() - mockDeviceInfo = mock() + deviceInfoPayloadStorage = mockk(relaxed = true) + mockClientServiceInternal = mockk(relaxed = true) + mockDeviceInfo = mockk(relaxed = true) setupMobileEngageComponent(FakeMobileEngageDependencyContainer()) @@ -48,35 +49,35 @@ class DeviceInfoStartActionTest : AnnotationSpec() { @Test fun testExecute_callsMobileEngageInternal_whenStorageIsEmpty() { - whenever(deviceInfoPayloadStorage.get()).thenReturn(null) + every { deviceInfoPayloadStorage.get() } returns null startAction.execute(null) waitForTask() - verify(mockClientServiceInternal).trackDeviceInfo(null) + verify { (mockClientServiceInternal).trackDeviceInfo(null) } } @Test fun testExecute_callsMobileEngageInternal_whenStorageHasChanged() { - whenever(deviceInfoPayloadStorage.get()).thenReturn(createDeviceInfoPayload()) - whenever(mockDeviceInfo.deviceInfoPayload).thenReturn(createOtherDeviceInfoPayload()) + every { deviceInfoPayloadStorage.get() } returns createDeviceInfoPayload() + every { mockDeviceInfo.deviceInfoPayload } returns createOtherDeviceInfoPayload() startAction.execute(null) waitForTask() - verify(mockClientServiceInternal).trackDeviceInfo(null) + verify { mockClientServiceInternal.trackDeviceInfo(null) } } @Test fun testExecute_shouldNotCallsMobileEngageInternal_whenStorageHasNotChangedAndExists() { - whenever(deviceInfoPayloadStorage.get()).thenReturn(createDeviceInfoPayload()) + every { deviceInfoPayloadStorage.get() } returns createDeviceInfoPayload() - whenever(mockDeviceInfo.deviceInfoPayload).thenReturn(createDeviceInfoPayload()) + every { mockDeviceInfo.deviceInfoPayload } returns createDeviceInfoPayload() startAction.execute(null) waitForTask() - verifyNoInteractions(mockClientServiceInternal) + verify(exactly = 0) { mockClientServiceInternal.trackDeviceInfo(any()) } } private fun createDeviceInfoPayload(): String { diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/AppStartActionTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/AppStartActionTest.kt index a1c66dce..bba30237 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/AppStartActionTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/AppStartActionTest.kt @@ -8,10 +8,9 @@ import com.emarsys.mobileengage.event.EventServiceInternal import com.emarsys.mobileengage.fake.FakeMobileEngageDependencyContainer import com.emarsys.mobileengage.util.waitForTask import com.emarsys.testUtil.AnnotationSpec -import com.emarsys.testUtil.mockito.whenever -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify class AppStartActionTest : AnnotationSpec() { @@ -22,8 +21,8 @@ class AppStartActionTest : AnnotationSpec() { @Before fun setUp() { - mockEventServiceInternal = mock() - mockContactTokenStorage = mock() + mockEventServiceInternal = mockk(relaxed = true) + mockContactTokenStorage = mockk(relaxed = true) setupMobileEngageComponent(FakeMobileEngageDependencyContainer()) @@ -37,21 +36,37 @@ class AppStartActionTest : AnnotationSpec() { @Test fun testExecute_callsEventServiceInternal_whenContactTokenIsPresent() { - whenever(mockContactTokenStorage.get()).thenReturn("contactToken") + every { mockContactTokenStorage.get() } returns "contactToken" startAction.execute(null) waitForTask() - verify(mockEventServiceInternal).trackInternalCustomEventAsync("app:start", null, null) + verify { (mockEventServiceInternal).trackInternalCustomEventAsync("app:start", null, null) } } @Test fun testExecute_EventServiceInternal_shouldNotBeCalled_whenContactTokenIsNotPresent() { - whenever(mockContactTokenStorage.get()).thenReturn(null) + every { mockContactTokenStorage.get() } returns null startAction.execute(null) waitForTask() - verifyNoInteractions(mockEventServiceInternal) + verify(exactly = 0) { + mockEventServiceInternal.trackInternalCustomEvent( + any(), + any(), + any() + ) + } + verify(exactly = 0) { + mockEventServiceInternal.trackInternalCustomEventAsync( + any(), + any(), + any() + ) + } + verify(exactly = 0) { mockEventServiceInternal.trackCustomEvent(any(), any(), any()) } + verify(exactly = 0) { mockEventServiceInternal.trackCustomEventAsync(any(), any(), any()) } + } } \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogProviderTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogProviderTest.kt index 90032fda..381725bf 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogProviderTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogProviderTest.kt @@ -1,10 +1,8 @@ package com.emarsys.mobileengage.iam.dialog -import androidx.test.core.app.ActivityScenario import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.testUtil.AnnotationSpec -import com.emarsys.testUtil.fake.FakeActivity import io.kotest.matchers.shouldBe import org.mockito.kotlin.mock import java.util.concurrent.CountDownLatch @@ -13,25 +11,16 @@ import kotlin.concurrent.thread class IamDialogProviderTest : AnnotationSpec() { private lateinit var iamDialogProvider: IamDialogProvider - private lateinit var scenario: ActivityScenario @Before fun setUp() { - scenario = ActivityScenario.launch(FakeActivity::class.java) - scenario.onActivity { activity -> - iamDialogProvider = IamDialogProvider( - ConcurrentHandlerHolderFactory.create(), - mock(), - mock(), - mock(), - mock() - ) - } - } - - @After - fun tearDown() { - scenario.close() + iamDialogProvider = IamDialogProvider( + ConcurrentHandlerHolderFactory.create(), + mock(), + mock(), + mock(), + mock() + ) } @Test diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt index 7b455040..5c48978b 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/dialog/IamDialogTest.kt @@ -1,6 +1,5 @@ package com.emarsys.mobileengage.iam.dialog - import android.content.pm.ActivityInfo import android.os.Bundle import android.webkit.WebView @@ -34,12 +33,9 @@ import com.emarsys.testUtil.ReflectionTestUtils import com.emarsys.testUtil.fake.FakeActivity import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import java.util.concurrent.CountDownLatch class IamDialogTest : AnnotationSpec() { @@ -62,59 +58,66 @@ class IamDialogTest : AnnotationSpec() { private lateinit var mockCurrentActivityProvider: CurrentActivityProvider private lateinit var iamDialog: IamDialog - private lateinit var scenario: ActivityScenario + private var scenario: ActivityScenario? = null + private lateinit var iamWebView: IamWebView @Before fun setUp() { - scenario = ActivityScenario.launch(FakeActivity::class.java) - val countDownLatch = CountDownLatch(1) - scenario.onActivity { activity -> - mockTimestampProvider = mock() - val mockUuidProvider: UUIDProvider = mock { - on { provideId() } doReturn "uuid" - } - - mockJsBridge = mock() - mockJsBridgeFactory = mock { - on { createJsBridge(any()) } doReturn mockJsBridge - } - - mockJSCommandFactory = mock() - mockJSCommandFactoryProvider = mock { - on { provide() } doReturn mockJSCommandFactory - } - - mockConcurrentHandlerHolder = mock() - mockCurrentActivityProvider = mock { - on { get() } doReturn activity - } - - val iamWebView = createWebView() - mockWebViewFactory = mock { - on { create(mock()) } doReturn iamWebView - } - - setupMobileEngageComponent( - FakeMobileEngageDependencyContainer( - timestampProvider = mockTimestampProvider, - uuidProvider = mockUuidProvider, - webViewFactory = mockWebViewFactory, - jsCommandFactoryProvider = mockJSCommandFactoryProvider, - iamJsBridgeFactory = mockJsBridgeFactory, - concurrentHandlerHolder = mockConcurrentHandlerHolder, - currentActivityProvider = mockCurrentActivityProvider - ) + mockTimestampProvider = mockk(relaxed = true) + val mockUuidProvider: UUIDProvider = mockk(relaxed = true) + every { + mockUuidProvider.provideId() + } returns "uuid" + + mockJsBridge = mockk(relaxed = true) + mockJsBridgeFactory = mockk(relaxed = true) + every { + mockJsBridgeFactory.createJsBridge(any()) + } returns mockJsBridge + + mockJSCommandFactory = mockk(relaxed = true) + mockJSCommandFactoryProvider = mockk(relaxed = true) + every { + mockJSCommandFactoryProvider.provide() + } returns mockJSCommandFactory + + mockConcurrentHandlerHolder = mockk(relaxed = true) + mockWebViewFactory = mockk(relaxed = true) + mockCurrentActivityProvider = mockk(relaxed = true) + setupMobileEngageComponent( + FakeMobileEngageDependencyContainer( + timestampProvider = mockTimestampProvider, + uuidProvider = mockUuidProvider, + webViewFactory = mockWebViewFactory, + jsCommandFactoryProvider = mockJSCommandFactoryProvider, + iamJsBridgeFactory = mockJsBridgeFactory, + concurrentHandlerHolder = mockConcurrentHandlerHolder, + currentActivityProvider = mockCurrentActivityProvider ) - iamDialog = IamDialog(mockTimestampProvider, mockWebViewFactory) - countDownLatch.countDown() - } - countDownLatch.await() + ) + iamDialog = IamDialog(mockTimestampProvider, mockWebViewFactory) } @After fun tearDown() { tearDownMobileEngageComponent() - scenario.close() + scenario?.close() + } + + private fun launchFakeActivityIfNeeded() { + if (scenario == null) { + scenario = ActivityScenario.launch(FakeActivity::class.java) + val countDownLatch = CountDownLatch(1) + scenario!!.onActivity { activity -> + every { + mockCurrentActivityProvider.get() + } returns activity + + iamWebView = createWebView() + countDownLatch.countDown() + } + countDownLatch.await() + } } @Test @@ -129,12 +132,10 @@ class IamDialogTest : AnnotationSpec() { fun testCreate_shouldReturnIamDialogInstance() { val fragmentScenario = launchFragment { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } fragmentScenario.onFragment { fragment -> + (fragment is IamDialog) shouldBe true fragment shouldNotBe null } } @@ -148,10 +149,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putString(IamDialog.URL, null) bundle.putString(IamDialog.REQUEST_ID, null) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } fragmentScenario.onFragment { fragment -> val result = fragment.arguments @@ -171,10 +169,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putString(IamDialog.URL, null) bundle.putString(IamDialog.REQUEST_ID, requestId) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } fragmentScenario.onFragment { fragment -> val result = fragment.arguments @@ -195,10 +190,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putString(IamDialog.REQUEST_ID, requestId) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } fragmentScenario.onFragment { fragment -> val result = fragment.arguments @@ -219,10 +211,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putString(IamDialog.REQUEST_ID, requestId) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } fragmentScenario.onFragment { fragment -> val result = fragment.arguments @@ -241,10 +230,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putString(IamDialog.URL, URL) bundle.putString(IamDialog.REQUEST_ID, null) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } fragmentScenario.onFragment { fragment -> val result = fragment.arguments @@ -256,6 +242,7 @@ class IamDialogTest : AnnotationSpec() { @Test fun testInitialization_setsDimAmountToZero() { + launchFakeActivityIfNeeded() val bundle = Bundle() bundle.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) @@ -263,10 +250,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putString(IamDialog.URL, URL) bundle.putString(IamDialog.REQUEST_ID, null) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } displayDialog(fragmentScenario) fragmentScenario.onFragment { @@ -276,8 +260,11 @@ class IamDialogTest : AnnotationSpec() { } } + @Test fun testInitialization_setsDialogToFullscreen() { + launchFakeActivityIfNeeded() + val bundle = Bundle() bundle.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) bundle.putString(IamDialog.SID, SID) @@ -285,10 +272,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putString(IamDialog.REQUEST_ID, null) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } displayDialog(fragmentScenario) @@ -306,6 +290,8 @@ class IamDialogTest : AnnotationSpec() { @Test fun testDialog_stillVisible_afterOrientationChange() { + launchFakeActivityIfNeeded() + val bundle = Bundle() bundle.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) bundle.putString(IamDialog.SID, SID) @@ -314,10 +300,7 @@ class IamDialogTest : AnnotationSpec() { bundle.putSerializable("loading_time", InAppLoadingTime(0, 0)) val fragmentScenario = launchFragment(bundle) { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } displayDialog(fragmentScenario) @@ -343,6 +326,8 @@ class IamDialogTest : AnnotationSpec() { @Test fun testDialog_cancel_turnsRetainInstanceOff() { + launchFakeActivityIfNeeded() + val bundle = Bundle() bundle.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) bundle.putString(IamDialog.SID, SID) @@ -372,9 +357,10 @@ class IamDialogTest : AnnotationSpec() { fragmentLatch.await() } - @Test fun testDialog_dismiss_turnsRetainInstanceOff() { + launchFakeActivityIfNeeded() + val bundle = Bundle() bundle.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) bundle.putString(IamDialog.SID, SID) @@ -407,126 +393,116 @@ class IamDialogTest : AnnotationSpec() { fun testOnResume_callsActions_ifProvided() { val args = Bundle() args.putString(CAMPAIGN_ID_KEY, "123456789") - val actions: List = listOf(mock(), mock(), mock()) + val actions: List = + listOf(mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true)) - val fragmentLatch = CountDownLatch(1) - val fragmentScenario = launchFragment(args) { - iamDialog.apply { - setActions(actions) - } + iamDialog.apply { + setActions(actions) + arguments = args } - displayDialog(fragmentScenario) + iamDialog.onResume() - fragmentScenario.onFragment { - for (action in actions) { - verify(action).execute("123456789", null, null) - } - fragmentLatch.countDown() + for (action in actions) { + verify { (action).execute("123456789", null, null) } } - fragmentLatch.await() + } @Test fun testOnResume_callsActions_onlyOnce() { - val actions: List = listOf(mock(), mock(), mock()) + val actions: List = + listOf(mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true)) val args = Bundle() args.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) args.putString(IamDialog.SID, SID) args.putString(IamDialog.URL, URL) - val fragmentScenario = launchFragment(args) { - iamDialog.apply { - setActions(actions) - } + + iamDialog.apply { + setActions(actions) + arguments = args } - fragmentScenario.moveToState(Lifecycle.State.CREATED) - fragmentScenario.moveToState(Lifecycle.State.RESUMED) - fragmentScenario.moveToState(Lifecycle.State.CREATED) - fragmentScenario.moveToState(Lifecycle.State.RESUMED) - fragmentScenario.onFragment { - it.activity?.let { activity -> - iamDialog.loadInApp( - "", - InAppMetaData("123456789", null, null), - MessageLoadedListener {}, - activity - ) - } - for (action in actions) { - verify(action, times(1)).execute(any(), any(), any()) - } + iamDialog.onResume() + iamDialog.onResume() + + iamDialog.loadInApp( + "", + InAppMetaData("123456789", null, null), + MessageLoadedListener {}, + mockk(relaxed = true) + ) + + for (action in actions) { + verify(exactly = 1) { action.execute(any(), any(), any()) } } } @Test fun testOnScreenTime_savesDuration_betweenResumeAndPause() { - whenever(mockTimestampProvider.provideTimestamp()).thenReturn(100L, 250L) + every { mockTimestampProvider.provideTimestamp() } returns 100L andThen 250L val args = Bundle() args.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) args.putString(IamDialog.SID, SID) args.putString(IamDialog.URL, URL) - val fragmentScenario = launchFragment(args) { - iamDialog - } - fragmentScenario.onFragment { - it.activity?.let { activity -> - iamDialog.loadInApp( - "", - InAppMetaData("123456789", null, null), - MessageLoadedListener {}, - activity - ) - } - it.activity?.runOnUiThread { - it.onPause() - } - val onScreenTime = it.arguments?.getLong(ON_SCREEN_TIME_KEY) ?: -1 + iamDialog.arguments = args + iamDialog.onResume() - it.arguments shouldNotBe null - onScreenTime shouldBe 150 + iamDialog.loadInApp( + "", + InAppMetaData("123456789", null, null), + MessageLoadedListener {}, + mockk(relaxed = true) + ) - } + iamDialog.onPause() + + val onScreenTime = iamDialog.arguments?.getLong(ON_SCREEN_TIME_KEY) ?: -1 + + iamDialog.arguments shouldNotBe null + onScreenTime shouldBe 150 } @Test fun testOnScreenTime_aggregatesDurations_betweenMultipleResumeAndPause() { - whenever(mockTimestampProvider.provideTimestamp()).thenReturn(100L, 250L, 1000L, 1003L) + every { mockTimestampProvider.provideTimestamp() } returnsMany listOf( + 100, + 250L, + 1000L, + 1003 + ) val args = Bundle() args.putString(IamDialog.CAMPAIGN_ID, CAMPAIGN_ID) args.putString(IamDialog.SID, SID) args.putString(IamDialog.URL, URL) - val fragmentScenario = launchFragment(args) { - iamDialog - } - fragmentScenario.onFragment { - it.activity?.let { activity -> - iamDialog.loadInApp( - "", - InAppMetaData("123456789", null, null), - MessageLoadedListener {}, - activity - ) - } - it.activity?.runOnUiThread { - it.onPause() - } - it.arguments shouldNotBe null - it.arguments!!.getLong(ON_SCREEN_TIME_KEY) shouldBe 150 + iamDialog.arguments = args + iamDialog.onResume() + + iamDialog.loadInApp( + "", + InAppMetaData("123456789", null, null), + {}, + mockk(relaxed = true) + ) + + iamDialog.onPause() + iamDialog.arguments shouldNotBe null + iamDialog.arguments!!.getLong(ON_SCREEN_TIME_KEY) shouldBe 150 + + iamDialog.onResume() + iamDialog.onPause() + + iamDialog.arguments shouldNotBe null + iamDialog.arguments!!.getLong(ON_SCREEN_TIME_KEY) shouldBe 153 - it.activity?.runOnUiThread { - it.onResume() - it.onPause() - } - it.arguments shouldNotBe null - it.arguments!!.getLong(ON_SCREEN_TIME_KEY) shouldBe 153 - } } @Test fun testOnStart_shouldNotThrowTheSpecifiedChildAlreadyHasAParent_exception() { + launchFakeActivityIfNeeded() + var result: Exception? = null try { val fragmentScenario = launchFragment { @@ -545,23 +521,22 @@ class IamDialogTest : AnnotationSpec() { @Test fun testOnStart_shouldNotThrowTheSpecifiedWebViewAlreadyHasAParent_exception() { + launchFakeActivityIfNeeded() + var result: Exception? = null try { - val webView = runOnMain { + + runOnMain { val webView = WebView(getTargetContext()) LinearLayout(getTargetContext()).addView(webView) - webView + + iamWebView.webView = webView } - val iamWebView = createWebView() - iamWebView.webView = webView - whenever(mockWebViewFactory.create(mock())).thenReturn(iamWebView) + every { mockWebViewFactory.create(any()) } returns iamWebView val fragmentScenario = launchFragment { - IamDialog( - mobileEngage().timestampProvider, - mobileEngage().webViewFactory - ) + iamDialog } displayDialog(fragmentScenario) fragmentScenario.onFragment { @@ -575,12 +550,14 @@ class IamDialogTest : AnnotationSpec() { @Test fun testLoadInApp() { + launchFakeActivityIfNeeded() + val html = "" val inAppMetaData = InAppMetaData(CAMPAIGN_ID, null, null) val messageLoadedListener = MessageLoadedListener { } - val mockIamWebView: IamWebView = mock() - whenever(mockWebViewFactory.create(mock())).thenReturn(mockIamWebView) + val mockIamWebView: IamWebView = mockk(relaxed = true) + every { mockWebViewFactory.create(mockk(relaxed = true)) } returns mockIamWebView val dialog = IamDialog( mobileEngage().timestampProvider, @@ -589,9 +566,9 @@ class IamDialogTest : AnnotationSpec() { ReflectionTestUtils.setInstanceField(dialog, "iamWebView", mockIamWebView) - dialog.loadInApp(html, inAppMetaData, messageLoadedListener, mock()) + dialog.loadInApp(html, inAppMetaData, messageLoadedListener, mockk(relaxed = true)) - verify(mockIamWebView).load(html, inAppMetaData, messageLoadedListener) + verify { mockIamWebView.load(html, inAppMetaData, messageLoadedListener) } } private fun createWebView(): IamWebView { diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/IamJsBridgeTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/IamJsBridgeTest.kt index 4bb812b4..d7d3a3f9 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/IamJsBridgeTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/IamJsBridgeTest.kt @@ -1,29 +1,19 @@ package com.emarsys.mobileengage.iam.jsbridge - -import androidx.test.core.app.ActivityScenario import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.handler.ConcurrentHandlerHolder import com.emarsys.mobileengage.api.event.EventHandler import com.emarsys.mobileengage.iam.model.InAppMetaData import com.emarsys.mobileengage.iam.webview.IamWebView import com.emarsys.testUtil.AnnotationSpec -import com.emarsys.testUtil.fake.FakeActivity import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify import org.json.JSONObject -import org.mockito.ArgumentCaptor -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.capture -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.isNull -import org.mockito.kotlin.mock -import org.mockito.kotlin.timeout -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.verifyNoMoreInteractions - class IamJsBridgeTest : AnnotationSpec() { @@ -47,81 +37,73 @@ class IamJsBridgeTest : AnnotationSpec() { private lateinit var mockOnOpenExternalUrlListener: JSCommand private lateinit var mockCopyToClipboardListener: JSCommand private lateinit var mockOnMEEventListener: JSCommand - private lateinit var captor: ArgumentCaptor - private lateinit var scenario: ActivityScenario + private val slot = slot() @Before fun setUp() { - scenario = ActivityScenario.launch(FakeActivity::class.java) - scenario.onActivity { activity -> - inAppMetaData = InAppMetaData("campaignId", "sid", "url") - concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() - mockIamWebView = mock() - mockOnCloseListener = mock() - mockOnAppEventListener = mock() - mockOnButtonClickedListener = mock() - mockOnOpenExternalUrlListener = mock() - mockOnMEEventListener = mock() - mockCopyToClipboardListener = mock() - mockJsCommandFactory = mock { - on { create(JSCommandFactory.CommandType.ON_CLOSE) } doReturn (mockOnCloseListener) - on { create(JSCommandFactory.CommandType.ON_ME_EVENT) } doReturn (mockOnMEEventListener) - on { create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) } doReturn (mockOnOpenExternalUrlListener) - on { create(JSCommandFactory.CommandType.ON_APP_EVENT) } doReturn (mockOnAppEventListener) - on { - create( - JSCommandFactory.CommandType.ON_BUTTON_CLICKED - ) - } doReturn (mockOnButtonClickedListener) - on { create(JSCommandFactory.CommandType.ON_COPY_TO_CLIPBOARD) } doReturn mockCopyToClipboardListener - } - mockEventHandler = mock() - jsBridge = IamJsBridge( - concurrentHandlerHolder, - mockJsCommandFactory - ) - jsBridge.iamWebView = mockIamWebView - captor = ArgumentCaptor.forClass(JSONObject::class.java) + inAppMetaData = InAppMetaData("campaignId", "sid", "url") + concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() + mockIamWebView = mockk(relaxed = true) + mockOnCloseListener = mockk(relaxed = true) + mockOnAppEventListener = mockk(relaxed = true) + mockOnButtonClickedListener = mockk(relaxed = true) + mockOnOpenExternalUrlListener = mockk(relaxed = true) + mockOnMEEventListener = mockk(relaxed = true) + mockCopyToClipboardListener = mockk(relaxed = true) + mockJsCommandFactory = mockk { + every { create(JSCommandFactory.CommandType.ON_CLOSE) } returns mockOnCloseListener + every { create(JSCommandFactory.CommandType.ON_ME_EVENT) } returns mockOnMEEventListener + every { create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) } returns mockOnOpenExternalUrlListener + every { create(JSCommandFactory.CommandType.ON_APP_EVENT) } returns mockOnAppEventListener + every { create(JSCommandFactory.CommandType.ON_BUTTON_CLICKED) } returns mockOnButtonClickedListener + every { create(JSCommandFactory.CommandType.ON_COPY_TO_CLIPBOARD) } returns mockCopyToClipboardListener } - } - - @After - fun tearDown() { - scenario.close() + mockEventHandler = mockk(relaxed = true) + jsBridge = IamJsBridge( + concurrentHandlerHolder, + mockJsCommandFactory + ) + jsBridge.iamWebView = mockIamWebView } @Test fun testClose_shouldInvokeOnCloseListener_createdByFactory() { jsBridge.close(jsonObject.toString()) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_CLOSE) - verify(mockOnCloseListener).invoke(anyOrNull(), any()) + verify { + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_CLOSE) + mockOnCloseListener.invoke(null, any()) + } } @Test fun testOnAppEvent_shouldInvokeOnAppEventListener_createdByFactory() { jsBridge.triggerAppEvent(jsonObject.toString()) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_APP_EVENT) - verify(mockOnAppEventListener).invoke(anyOrNull(), any()) + verify { + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_APP_EVENT) + mockOnAppEventListener.invoke("testName", any()) + } } @Test fun testOnButtonClickedEvent_shouldInvokeOnAppEventListener_createdByFactory() { jsBridge.buttonClicked(jsonObject.toString()) - verify(mockJsCommandFactory).create( - JSCommandFactory.CommandType.ON_BUTTON_CLICKED - ) - verify(mockOnButtonClickedListener, timeout(2500)).invoke(anyOrNull(), any()) + verify(timeout = 2500) { + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_BUTTON_CLICKED) + mockOnButtonClickedListener.invoke("testButtonId", any()) + } } @Test fun testOnMEEvent_shouldInvokeOnAppEventListener_createdByFactory() { jsBridge.triggerMEEvent(jsonObject.toString()) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_ME_EVENT) - verify(mockOnMEEventListener).invoke(anyOrNull(), any()) + verify { + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_ME_EVENT) + mockOnMEEventListener.invoke("testName", any()) + } } @Test @@ -137,10 +119,12 @@ class IamJsBridgeTest : AnnotationSpec() { ) jsBridge.openExternalLink(json.toString()) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_CLOSE) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) - verify(mockOnCloseListener).invoke(isNull(), any()) - verify(mockOnOpenExternalUrlListener).invoke(anyOrNull(), any()) + verify { + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_CLOSE) + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) + mockOnCloseListener.invoke(null, any()) + mockOnOpenExternalUrlListener.invoke("https://emarsys.com", any()) + } } @Test @@ -155,10 +139,12 @@ class IamJsBridgeTest : AnnotationSpec() { ) jsBridge.openExternalLink(json.toString()) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_CLOSE) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) - verify(mockOnCloseListener).invoke(isNull(), any()) - verify(mockOnOpenExternalUrlListener).invoke(anyOrNull(), any()) + verify { + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_CLOSE) + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) + mockOnCloseListener.invoke(null, any()) + mockOnOpenExternalUrlListener.invoke("https://emarsys.com", any()) + } } @Test @@ -174,10 +160,11 @@ class IamJsBridgeTest : AnnotationSpec() { ) jsBridge.openExternalLink(json.toString()) - verify(mockJsCommandFactory).create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) - verifyNoMoreInteractions(mockJsCommandFactory) - verifyNoInteractions(mockOnCloseListener) - verify(mockOnOpenExternalUrlListener).invoke(anyOrNull(), any()) + verify { + mockJsCommandFactory.create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) + } + verify { mockOnCloseListener wasNot Called } + verify { mockOnOpenExternalUrlListener.invoke("https://emarsys.com", any()) } } @Test @@ -187,13 +174,12 @@ class IamJsBridgeTest : AnnotationSpec() { jsBridge.triggerAppEvent(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe true + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe true } @Test @@ -203,13 +189,12 @@ class IamJsBridgeTest : AnnotationSpec() { jsBridge.triggerMEEvent(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe true + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe true } @Test @@ -219,13 +204,12 @@ class IamJsBridgeTest : AnnotationSpec() { val json = JSONObject().put("id", id).put("buttonId", buttonId) jsBridge.buttonClicked(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe true + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe true } @Test @@ -235,13 +219,12 @@ class IamJsBridgeTest : AnnotationSpec() { val json = JSONObject().put("id", id).put("url", url).put("keepInAppOpen", false) jsBridge.openExternalLink(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe true + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe true } @Test @@ -250,14 +233,13 @@ class IamJsBridgeTest : AnnotationSpec() { val json = JSONObject().put("id", id) jsBridge.triggerAppEvent(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe false - captor.value["error"] shouldBe "Missing name!" + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe false + slot.captured["error"] shouldBe "Missing name!" } @Test @@ -267,14 +249,13 @@ class IamJsBridgeTest : AnnotationSpec() { jsBridge.triggerMEEvent(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe false - captor.value["error"] shouldBe "Missing name!" + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe false + slot.captured["error"] shouldBe "Missing name!" } @Test @@ -283,31 +264,28 @@ class IamJsBridgeTest : AnnotationSpec() { val json = JSONObject().put("id", id) jsBridge.buttonClicked(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe false - captor.value["error"] shouldBe "Missing buttonId!" + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe false + slot.captured["error"] shouldBe "Missing buttonId!" } - @Test fun testOpenExternalLink_shouldInvokeCallback_whenUrlIsMissing() { val id = "12346789" val json = JSONObject().put("id", id).put("keepInAppOpen", false) jsBridge.openExternalLink(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe false - captor.value["error"] shouldBe "Missing url!" + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe false + slot.captured["error"] shouldBe "Missing url!" } @Test @@ -316,13 +294,12 @@ class IamJsBridgeTest : AnnotationSpec() { val json = JSONObject().put("id", id).put("text", "testText") jsBridge.copyToClipboard(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe true + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe true } @Test @@ -331,13 +308,12 @@ class IamJsBridgeTest : AnnotationSpec() { val json = JSONObject().put("id", id) jsBridge.copyToClipboard(json.toString()) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(capture(captor)) + verify(timeout = 1000) { + mockIamWebView.respondToJS(capture(slot)) + } - captor.value["id"] shouldBe id - captor.value["success"] shouldBe false + slot.captured["id"] shouldBe id + slot.captured["success"] shouldBe false } @Test @@ -352,9 +328,8 @@ class IamJsBridgeTest : AnnotationSpec() { val json = JSONObject().put("id", "123456789").put("key", "value") jsBridge.sendResult(json) - verify( - mockIamWebView, - timeout(1000) - ).respondToJS(json) + verify(timeout = 1000) { + mockIamWebView.respondToJS(json) + } } } \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/JSCommandFactoryTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/JSCommandFactoryTest.kt index b5fe608c..c437eb8e 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/JSCommandFactoryTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/jsbridge/JSCommandFactoryTest.kt @@ -1,9 +1,7 @@ package com.emarsys.mobileengage.iam.jsbridge - import android.app.Activity import android.content.ClipboardManager -import androidx.test.core.app.ActivityScenario import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.database.repository.Repository import com.emarsys.core.database.repository.SqlSpecification @@ -14,26 +12,19 @@ import com.emarsys.mobileengage.iam.InAppInternal import com.emarsys.mobileengage.iam.model.InAppMetaData import com.emarsys.mobileengage.iam.model.buttonclicked.ButtonClicked import com.emarsys.testUtil.AnnotationSpec -import com.emarsys.testUtil.fake.FakeActivity import com.emarsys.testUtil.mockito.ThreadSpy import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.runBlocking import org.json.JSONObject -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.isNull -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever import java.util.concurrent.CountDownLatch class JSCommandFactoryTest : AnnotationSpec() { + private companion object { const val PROPERTY = "testProperty" const val TIMESTAMP = 1233L @@ -41,7 +32,6 @@ class JSCommandFactoryTest : AnnotationSpec() { const val SID = "sid" const val URL = "url" const val TEST_URL = "https://emarsys.com" - } private lateinit var jsCommandFactory: JSCommandFactory @@ -52,54 +42,40 @@ class JSCommandFactoryTest : AnnotationSpec() { private lateinit var mockOnCloseListener: OnCloseListener private lateinit var mockOnAppEventListener: OnAppEventListener private lateinit var mockTimestampProvider: TimestampProvider - private lateinit var fakeActivity: Activity private lateinit var mockClipboardManager: ClipboardManager - private lateinit var scenario: ActivityScenario @Before fun setUp() { - scenario = ActivityScenario.launch(FakeActivity::class.java) - val countDownLatch = CountDownLatch(1) - scenario.onActivity { activity -> - fakeActivity = activity - mockCurrentActivityProvider = mock() - whenever(mockCurrentActivityProvider.get()).thenReturn(fakeActivity) - - mockClipboardManager = mock() - concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() - mockInAppInternal = mock() - mockButtonClickedRepository = mock() - mockOnCloseListener = mock() - mockOnAppEventListener = mock() - mockTimestampProvider = mock() - - whenever(mockTimestampProvider.provideTimestamp()).thenReturn(TIMESTAMP) - - jsCommandFactory = JSCommandFactory( - mockCurrentActivityProvider, - concurrentHandlerHolder, - mockInAppInternal, - mockButtonClickedRepository, - mockOnCloseListener, - mockOnAppEventListener, - mockTimestampProvider, - mockClipboardManager - ) - countDownLatch.countDown() + mockCurrentActivityProvider = mockk() + concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() + mockInAppInternal = mockk(relaxed = true) + mockButtonClickedRepository = mockk(relaxed = true) + mockOnCloseListener = mockk(relaxed = true) + mockOnAppEventListener = mockk(relaxed = true) + mockTimestampProvider = mockk { + every { provideTimestamp() } returns TIMESTAMP } - countDownLatch.await() - } - - @After - fun tearDown() { - scenario.close() + mockClipboardManager = mockk(relaxed = true) + + jsCommandFactory = JSCommandFactory( + mockCurrentActivityProvider, + concurrentHandlerHolder, + mockInAppInternal, + mockButtonClickedRepository, + mockOnCloseListener, + mockOnAppEventListener, + mockTimestampProvider, + mockClipboardManager + ) } @Test fun testCreate_shouldTriggerCloseCommand_onMainThread() { - val threadSpy: ThreadSpy<*> = ThreadSpy() - whenever(mockOnCloseListener.invoke()).doAnswer(threadSpy) + every { mockOnCloseListener.invoke() } answers { + threadSpy.call() + Unit + } jsCommandFactory.create(JSCommandFactory.CommandType.ON_CLOSE).invoke(null, JSONObject()) @@ -110,13 +86,16 @@ class JSCommandFactoryTest : AnnotationSpec() { fun testCreate_shouldTriggerOnAppEventCommand_onMainThread() { val threadSpy: ThreadSpy<*> = ThreadSpy() val expectedJson = JSONObject(mapOf("testKey" to "testValue")) - whenever(mockOnAppEventListener.invoke(anyOrNull(), any())).doAnswer(threadSpy) + every { mockOnAppEventListener.invoke(any(), any()) } answers { + threadSpy.call() + Unit + } jsCommandFactory.create(JSCommandFactory.CommandType.ON_APP_EVENT) .invoke("test", expectedJson) threadSpy.verifyCalledOnMainThread() - verify(mockOnAppEventListener).invoke("test", expectedJson) + verify { mockOnAppEventListener.invoke("test", expectedJson) } } @Test @@ -125,23 +104,27 @@ class JSCommandFactoryTest : AnnotationSpec() { jsCommandFactory.inAppMetaData = inAppMetaData val latch1 = CountDownLatch(1) val latch2 = CountDownLatch(1) + concurrentHandlerHolder.coreHandler.post { latch1.await() } + jsCommandFactory.create(JSCommandFactory.CommandType.ON_BUTTON_CLICKED) .invoke(PROPERTY, JSONObject(mapOf("key" to "value"))) - verifyNoInteractions(mockButtonClickedRepository) - verifyNoInteractions(mockInAppInternal) + + verify { mockButtonClickedRepository wasNot Called } + verify { mockInAppInternal wasNot Called } + latch1.countDown() concurrentHandlerHolder.coreHandler.post { latch2.countDown() } latch2.await() + runBlocking { - verify(mockButtonClickedRepository).add( - ButtonClicked( - inAppMetaData.campaignId, - PROPERTY, - TIMESTAMP + verify { + mockButtonClickedRepository.add( + ButtonClicked(inAppMetaData.campaignId, PROPERTY, TIMESTAMP) ) - ) + } } + val expectedEventName = "inapp:click" val attributes = mapOf( "campaignId" to CAMPAIGN_ID, @@ -150,7 +133,7 @@ class JSCommandFactoryTest : AnnotationSpec() { "url" to URL ) - verify(mockInAppInternal).trackInternalCustomEvent(expectedEventName, attributes, null) + verify { mockInAppInternal.trackInternalCustomEvent(expectedEventName, attributes, null) } } @Test @@ -158,36 +141,38 @@ class JSCommandFactoryTest : AnnotationSpec() { val inAppMetaData = InAppMetaData(CAMPAIGN_ID, null, null) jsCommandFactory.inAppMetaData = inAppMetaData val latch = CountDownLatch(1) + jsCommandFactory.create(JSCommandFactory.CommandType.ON_BUTTON_CLICKED) .invoke(PROPERTY, JSONObject()) concurrentHandlerHolder.coreHandler.post { latch.countDown() } latch.await() + runBlocking { - verify(mockButtonClickedRepository).add( - ButtonClicked( - inAppMetaData.campaignId, - PROPERTY, - TIMESTAMP + verify { + mockButtonClickedRepository.add( + ButtonClicked(inAppMetaData.campaignId, PROPERTY, TIMESTAMP) ) - ) + } } + val expectedEventName = "inapp:click" val attributes = mapOf( "campaignId" to CAMPAIGN_ID, "buttonId" to PROPERTY ) - verify(mockInAppInternal).trackInternalCustomEvent(expectedEventName, attributes, null) + verify { mockInAppInternal.trackInternalCustomEvent(expectedEventName, attributes, null) } } @Test fun testCreate_OpenExternalUrl_shouldTriggerStartActivityOnMainThread_whenActivityIsNotNullAndActivityCanBeResolved() { val property = TEST_URL val threadSpy = ThreadSpy() - val mockActivity: Activity = mock() - whenever(mockCurrentActivityProvider.get()).thenReturn(mockActivity) - whenever(mockActivity.startActivity(any())).doAnswer { + val mockActivity: Activity = mockk(relaxed = true) + + every { mockCurrentActivityProvider.get() } returns mockActivity + every { mockActivity.startActivity(any()) } answers { threadSpy.call() Unit } @@ -195,20 +180,18 @@ class JSCommandFactoryTest : AnnotationSpec() { jsCommandFactory.create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) .invoke(property, JSONObject()) - verify(mockCurrentActivityProvider).get() - verify(mockActivity).startActivity(any()) + verify { mockCurrentActivityProvider.get() } + verify { mockActivity.startActivity(any()) } threadSpy.verifyCalledOnMainThread() } @Test fun testCreate_OpenExternalUrl_shouldThrowException_whenActivityIsNotNullAndActivityCanNotBeResolved() { val property = TEST_URL - val mockActivity: Activity = mock() - whenever(mockCurrentActivityProvider.get()).thenReturn(mockActivity) + val mockActivity: Activity = mockk(relaxed = true) - whenever(mockActivity.startActivity(any())).thenAnswer { - throw Exception() - } + every { mockCurrentActivityProvider.get() } returns mockActivity + every { mockActivity.startActivity(any()) } throws Exception() val openExternalUrlListener = jsCommandFactory.create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) @@ -216,13 +199,14 @@ class JSCommandFactoryTest : AnnotationSpec() { val exception = shouldThrow { openExternalUrlListener.invoke(property, JSONObject()) } + exception.message shouldBe "Url cannot be handled by any application!" } @Test fun testCreate_OpenExternalUrl_shouldThrowException_whenActivityIsNull() { val property = TEST_URL - whenever(mockCurrentActivityProvider.get()) doReturn null + every { mockCurrentActivityProvider.get() } returns null val openExternalUrlListener = jsCommandFactory.create(JSCommandFactory.CommandType.ON_OPEN_EXTERNAL_URL) @@ -230,6 +214,7 @@ class JSCommandFactoryTest : AnnotationSpec() { val exception = shouldThrow { openExternalUrlListener.invoke(property, JSONObject()) } + exception.message shouldBe "UI unavailable!" } @@ -239,24 +224,22 @@ class JSCommandFactoryTest : AnnotationSpec() { val latch2 = CountDownLatch(1) concurrentHandlerHolder.coreHandler.post { latch1.await() } + val meEventListener = jsCommandFactory.create(JSCommandFactory.CommandType.ON_ME_EVENT) val property = "testProperty" val payload = mapOf("payloadKey" to "payloadValue") + meEventListener.invoke( property, JSONObject(mapOf("key" to "value", "payload" to payload)) ) - verifyNoInteractions(mockInAppInternal) + verify { mockInAppInternal wasNot Called } latch1.countDown() concurrentHandlerHolder.coreHandler.post { latch2.countDown() } latch2.await() - verify(mockInAppInternal).trackCustomEventAsync( - eq(property), - eq(payload), - isNull() - ) + verify { mockInAppInternal.trackCustomEventAsync(eq(property), eq(payload), isNull()) } } @Test @@ -268,9 +251,9 @@ class JSCommandFactoryTest : AnnotationSpec() { .invoke(null, testJson) concurrentHandlerHolder.coreHandler.post { latch.countDown() } - latch.await() - verify(mockClipboardManager).setPrimaryClip(any()) + + verify { mockClipboardManager.setPrimaryClip(any()) } } @Test @@ -282,8 +265,8 @@ class JSCommandFactoryTest : AnnotationSpec() { .invoke(null, testJson) concurrentHandlerHolder.coreHandler.post { latch.countDown() } - latch.await() - verify(mockClipboardManager, times(0)).setPrimaryClip(any()) + + verify(exactly = 0) { mockClipboardManager.setPrimaryClip(any()) } } } \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt index 1b82e0dd..4a7b0bfd 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/iam/webview/IamWebViewFactoryTest.kt @@ -2,7 +2,6 @@ package com.emarsys.mobileengage.iam.webview import android.app.Activity -import androidx.test.core.app.ActivityScenario import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.handler.ConcurrentHandlerHolder import com.emarsys.mobileengage.iam.jsbridge.IamJsBridge @@ -11,13 +10,9 @@ import com.emarsys.mobileengage.iam.jsbridge.JSCommandFactory import com.emarsys.mobileengage.iam.jsbridge.JSCommandFactoryProvider import com.emarsys.testUtil.AnnotationSpec import com.emarsys.testUtil.ExtensionTestUtils.runOnMain -import com.emarsys.testUtil.fake.FakeActivity import io.kotest.matchers.shouldBe -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import java.lang.ref.WeakReference -import java.util.concurrent.CountDownLatch +import io.mockk.every +import io.mockk.mockk class IamWebViewFactoryTest : AnnotationSpec() { @@ -28,60 +23,43 @@ class IamWebViewFactoryTest : AnnotationSpec() { private lateinit var concurrentHandlerHolder: ConcurrentHandlerHolder private lateinit var webViewFactory: IamWebViewFactory - private lateinit var scenario: ActivityScenario - - private lateinit var activityReference: WeakReference + private lateinit var mockActivity: Activity @Before fun setUp() { - mockJsBridge = mock() - mockJsBridgeFactory = mock { - on { createJsBridge(any()) } doReturn mockJsBridge + mockJsBridge = mockk(relaxed = true) + mockJsBridgeFactory = mockk { + every { createJsBridge(any()) } returns mockJsBridge } - mockJSCommandFactory = mock() - mockJSCommandFactoryProvider = mock { - on { provide() } doReturn mockJSCommandFactory + mockJSCommandFactory = mockk() + mockJSCommandFactoryProvider = mockk { + every { provide() } returns mockJSCommandFactory } concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() - scenario = ActivityScenario.launch(FakeActivity::class.java) - scenario.onActivity { activity -> - activityReference = WeakReference(activity) - webViewFactory = IamWebViewFactory( - mockJsBridgeFactory, - mockJSCommandFactoryProvider, - concurrentHandlerHolder - ) - } - } - - @After - fun tearDown() { - scenario.close() + mockActivity = mockk(relaxed = true) + webViewFactory = IamWebViewFactory( + mockJsBridgeFactory, + mockJSCommandFactoryProvider, + concurrentHandlerHolder + ) } @Test fun testCreateWithNull() { val iamWebView = runOnMain { - webViewFactory.create(activityReference.get()!!) + webViewFactory.create(mockActivity) } iamWebView::class.java shouldBe IamWebView::class.java } @Test fun testCreateWithActivity() { - val scenario = ActivityScenario.launch(FakeActivity::class.java) - val countDownLatch = CountDownLatch(1) - scenario.onActivity { activity -> - val iamWebView = runOnMain { - webViewFactory.create(activity) - } - iamWebView::class.java shouldBe IamWebView::class.java - countDownLatch.countDown() + val iamWebView = runOnMain { + webViewFactory.create(mockActivity) } - countDownLatch.await() - scenario.close() + iamWebView::class.java shouldBe IamWebView::class.java } } \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/LaunchApplicationCommandTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/LaunchApplicationCommandTest.kt index ab7fe3fb..ae27d556 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/LaunchApplicationCommandTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/notification/command/LaunchApplicationCommandTest.kt @@ -2,46 +2,30 @@ package com.emarsys.mobileengage.notification.command import android.app.Activity -import android.app.Application import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle -import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ActivityScenario import com.emarsys.mobileengage.fake.FakeActivityLifecycleCallbacks import com.emarsys.mobileengage.notification.LaunchActivityCommandLifecycleCallbacksFactory import com.emarsys.testUtil.AnnotationSpec import com.emarsys.testUtil.InstrumentationRegistry.Companion.getTargetContext -import com.emarsys.testUtil.fake.FakeActivity import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import java.util.concurrent.CountDownLatch +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import java.util.concurrent.CountDownLatch class LaunchApplicationCommandTest : AnnotationSpec() { - - private lateinit var scenario: ActivityScenario private lateinit var mockProviderLaunchActivityCommand: LaunchActivityCommandLifecycleCallbacksFactory + private lateinit var mockActivity: Activity @Before fun setUp() { - scenario = ActivityScenario.launch(FakeActivity::class.java) - scenario.onActivity { activity -> - mockProviderLaunchActivityCommand = - Mockito.mock(LaunchActivityCommandLifecycleCallbacksFactory::class.java) - } - } - - @After - fun tearDown() { - scenario.close() + mockProviderLaunchActivityCommand = mockk(relaxed = true) + mockActivity = mockk(relaxed = true) } @Test @@ -71,74 +55,73 @@ class LaunchApplicationCommandTest : AnnotationSpec() { @Test fun testRun_startsActivity_withCorrectIntent() { - val captor = ArgumentCaptor.forClass(Intent::class.java) + val captor = slot() val launchIntentForPackage = Intent() - val pm: PackageManager = mock() - whenever(pm.getLaunchIntentForPackage(ArgumentMatchers.anyString())).thenReturn( - launchIntentForPackage - ) - val mockActivity: Activity = mock() - whenever(mockActivity.applicationContext).thenReturn(mock()) - whenever(mockActivity.packageManager).thenReturn(pm) - whenever(mockActivity.packageName).thenReturn("packageName") - val extras = Bundle() - extras.putLong("key1", 800) - extras.putString("key2", "value") - val remoteIntent = Intent() - remoteIntent.putExtras(extras) - val command: Runnable = - LaunchApplicationCommand( - remoteIntent, - mockActivity, - mockProviderLaunchActivityCommand - ) + val pm: PackageManager = mockk() + every { pm.getLaunchIntentForPackage(any()) } returns launchIntentForPackage + every { mockActivity.packageManager } returns pm + every { mockActivity.packageName } returns "packageName" + every { mockActivity.applicationContext } returns getTargetContext().applicationContext + + val extras = Bundle().apply { + putLong("key1", 800) + putString("key2", "value") + } + + val remoteIntent = Intent().apply { + putExtras(extras) + } + + val command = + LaunchApplicationCommand(remoteIntent, mockActivity, mockProviderLaunchActivityCommand) command.run() - verify(mockActivity).startActivity(captor.capture()) - val expectedBundle = launchIntentForPackage.extras - val resultBundle = captor.value.extras - resultBundle!!.keySet() shouldBe expectedBundle!!.keySet() - for (key in expectedBundle.keySet()) { - resultBundle[key] shouldBe expectedBundle[key] + verify { mockActivity.startActivity(capture(captor)) } + captor.captured.extras!!.keySet() shouldBe launchIntentForPackage.extras!!.keySet() + + for (key in launchIntentForPackage.extras!!.keySet()) { + captor.captured.extras!!.get(key) shouldBe launchIntentForPackage.extras!!.get(key) } } @Test fun testRun_startsActivity_withIncorrectIntent() { - val pm: PackageManager = mock() - val mockActivity: Activity = mock() - whenever(mockActivity.packageManager).thenReturn(pm) - whenever(mockActivity.packageName).thenReturn("packageName") - whenever(mockActivity.applicationContext).thenReturn(mock()) - - val extras = Bundle() - extras.putLong("key1", 800) - extras.putString("key2", "value") - val remoteIntent = Intent() - remoteIntent.putExtras(extras) - val command: Runnable = - LaunchApplicationCommand( - remoteIntent, - mockActivity, - mockProviderLaunchActivityCommand - ) + val pm: PackageManager = mockk() + every { mockActivity.packageManager } returns pm + every { mockActivity.packageName } returns "packageName" + + val extras = Bundle().apply { + putLong("key1", 800) + putString("key2", "value") + } + + val remoteIntent = Intent().apply { + putExtras(extras) + } + + val command = LaunchApplicationCommand( + remoteIntent, + getTargetContext(), + mockProviderLaunchActivityCommand + ) command.run() + + verify(exactly = 0) { mockActivity.startActivity(any()) } } @Test fun testLaunchActivity_shouldBlock() { - whenever(mockProviderLaunchActivityCommand.create(any())).thenAnswer { invocation -> - FakeActivityLifecycleCallbacks(onResume = { (invocation.getArgument(0) as CountDownLatch).countDown() }) + every { mockProviderLaunchActivityCommand.create(any()) } answers { + FakeActivityLifecycleCallbacks(onResume = { (it.invocation.args[0] as CountDownLatch).countDown() }) } - scenario.onActivity { activity -> - val command: Runnable = - LaunchApplicationCommand(Intent(), activity, mockProviderLaunchActivityCommand) - command.run() - } + val command = LaunchApplicationCommand( + Intent(), + getTargetContext(), + mockProviderLaunchActivityCommand + ) + command.run() - scenario.onActivity { activity -> - activity.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) shouldBe true - } + verify { mockProviderLaunchActivityCommand.create(any()) } } } \ No newline at end of file diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt index 1dd35b43..4a21e760 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/responsehandler/InAppMessageResponseHandlerTest.kt @@ -1,12 +1,9 @@ package com.emarsys.mobileengage.responsehandler - import android.content.ClipboardManager -import androidx.test.core.app.ActivityScenario import com.emarsys.core.activity.TransitionSafeCurrentActivityWatchdog import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory import com.emarsys.core.handler.ConcurrentHandlerHolder -import com.emarsys.core.provider.activity.CurrentActivityProvider import com.emarsys.core.provider.timestamp.TimestampProvider import com.emarsys.core.provider.uuid.UUIDProvider import com.emarsys.core.request.model.RequestModel @@ -17,14 +14,11 @@ import com.emarsys.mobileengage.iam.dialog.IamDialogProvider import com.emarsys.mobileengage.iam.jsbridge.IamJsBridge import com.emarsys.mobileengage.iam.jsbridge.IamJsBridgeFactory import com.emarsys.testUtil.AnnotationSpec -import com.emarsys.testUtil.fake.FakeActivity import io.kotest.matchers.shouldBe -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.spy -import org.mockito.kotlin.verify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify class InAppMessageResponseHandlerTest : AnnotationSpec() { @@ -36,48 +30,31 @@ class InAppMessageResponseHandlerTest : AnnotationSpec() { private lateinit var mockClipboardManager: ClipboardManager private lateinit var mockJsBridge: IamJsBridge private lateinit var mockCurrentActivityProvider: TransitionSafeCurrentActivityWatchdog - private lateinit var scenario: ActivityScenario @Before fun init() { - scenario = ActivityScenario.launch(FakeActivity::class.java) - scenario.onActivity { activity -> - concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() - mockCurrentActivityProvider = mock { - on { activity() } doReturn activity - } - mockJsBridge = mock() - mockClipboardManager = mock() - mockJsBridgeFactory = mock { - on { createJsBridge(any()) } doReturn mockJsBridge - } - mockDialog = mock() - val dialogProvider = mock { - on { - provideDialog( - any(), - anyOrNull(), - anyOrNull(), - any() - ) - } doReturn mockDialog - } + concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() + mockCurrentActivityProvider = mockk(relaxed = true) + mockJsBridge = mockk(relaxed = true) + mockClipboardManager = mockk(relaxed = true) + mockJsBridgeFactory = mockk { + every { createJsBridge(any()) } returns mockJsBridge + } + mockDialog = mockk(relaxed = true) - presenter = spy( - OverlayInAppPresenter( - concurrentHandlerHolder, - dialogProvider, - mock(), - mockCurrentActivityProvider, - ) - ) - handler = InAppMessageResponseHandler(presenter) + val dialogProvider = mockk { + every { provideDialog(any(), any(), any(), any()) } returns mockDialog } - } - @After - fun tearDown() { - scenario.close() + presenter = spyk( + OverlayInAppPresenter( + concurrentHandlerHolder, + dialogProvider, + mockk(relaxed = true), + mockCurrentActivityProvider, + ) + ) + handler = InAppMessageResponseHandler(presenter) } @Test @@ -110,15 +87,18 @@ class InAppMessageResponseHandlerTest : AnnotationSpec() { val responseBody = String.format("{'message': {'html':'%s', 'campaignId': '123'} }", html) val response = buildResponseModel(responseBody) handler.handleResponse(response) - verify(presenter).present( - "123", - null, - null, - response.requestModel.id, - response.timestamp, - html, - null - ) + + verify { + presenter.present( + "123", + null, + null, + response.requestModel.id, + response.timestamp, + html, + null + ) + } } private fun buildResponseModel(responseBody: String): ResponseModel { diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt index 514ad459..d6f8958a 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/service/MessagingServiceUtilsTest.kt @@ -5,6 +5,7 @@ import android.R import android.app.Notification import android.content.Context import android.content.res.Resources +import android.graphics.Bitmap import android.os.Build.VERSION_CODES import android.util.DisplayMetrics import androidx.core.app.NotificationCompat @@ -1082,7 +1083,7 @@ class MessagingServiceUtilsTest : AnnotationSpec() { @Test fun testStyleNotification_whenStyleIsThumbnail() { val mockBuilder: NotificationCompat.Builder = mock { - on { setLargeIcon(any()) } doReturn it + on { setLargeIcon(any()) } doReturn it on { setContentTitle(any()) } doReturn it on { setContentText(any()) } doReturn it on { setStyle(any()) } doReturn it @@ -1157,7 +1158,7 @@ class MessagingServiceUtilsTest : AnnotationSpec() { ignored: Class ) { val mockBuilder: NotificationCompat.Builder = mock { - on { setLargeIcon(org.mockito.kotlin.any()) } doReturn it + on { setLargeIcon(any()) } doReturn it on { setStyle(org.mockito.kotlin.any()) } doReturn it } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationStyle.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationStyle.kt index 003a0afa..815e423e 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationStyle.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/service/NotificationStyle.kt @@ -85,7 +85,7 @@ object DefaultStyle : NotificationStyle() { builder.setLargeIcon(image) .setStyle(NotificationCompat.BigPictureStyle() .bigPicture(image) - .bigLargeIcon(null) + .bigLargeIcon(null as Bitmap?) .setBigContentTitle(notificationData.title) .setSummaryText(notificationData.body)) } else { diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index f7df7cfe..d139db24 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -14,6 +14,7 @@ android { namespace = "com.emarsys.sample" defaultConfig { applicationId = "com.emarsys.sample" + targetSdk = libs.versions.android.targetSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt() multiDexEnabled = true diff --git a/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt index 1e080821..3e8fd544 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt @@ -134,6 +134,7 @@ class DashboardScreen( if (!viewModel.isGeofenceEnabled()) { Emarsys.geofence.enable { if (it != null) { + it.printStackTrace() customTextToast( context, context.getString(R.string.something_went_wrong) From ea27f6b05f0a2bf066e02b9c6ddf8e4983494035 Mon Sep 17 00:00:00 2001 From: megamegax Date: Wed, 4 Sep 2024 10:30:39 +0200 Subject: [PATCH 33/33] fix(geofence): handle flags properly SUITEDEV-36519 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> --- .../geofence/DefaultGeofenceInternalTest.kt | 70 +++++++++++++++---- .../geofence/FetchGeofencesActionTest.kt | 10 +-- .../geofence/DefaultGeofenceInternal.kt | 16 +++-- .../geofence/GeofencePendingIntentProvider.kt | 7 +- 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternalTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternalTest.kt index 60ddfc72..19429c48 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternalTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternalTest.kt @@ -8,6 +8,7 @@ import android.content.pm.PackageManager import android.location.Location import android.location.LocationManager import android.os.Build +import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.emarsys.core.api.MissingPermissionException import com.emarsys.core.concurrency.ConcurrentHandlerHolderFactory @@ -263,15 +264,36 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { } @Test - fun testEnable_registersGeofenceBroadcastReceiver() { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.enable(null) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU) + fun testEnable_registersGeofenceBroadcastReceiverAboveTiramisu() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + geofenceInternalWithMockContext.enable(null) + geofenceInternalWithMockContext.enable(null) - verify( - mockContext, - timeout(100).times(1) - ).registerReceiver(any(), any()) + verify( + mockContext, + timeout(100).times(1) + ).registerReceiver( + any(), + any(), + eq(Context.RECEIVER_EXPORTED) + ) + + } + } + @Test + @SdkSuppress(maxSdkVersion = 32) + fun testEnable_registersGeofenceBroadcastReceiverBelowTiramisu() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + geofenceInternalWithMockContext.enable(null) + geofenceInternalWithMockContext.enable(null) + + verify( + mockContext, + timeout(100).times(1) + ).registerReceiver(any(), any()) + } } @Test @@ -309,15 +331,35 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { } @Test - fun testDisable() { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.disable() - geofenceInternalWithMockContext.enable(null) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU) + fun testDisableAboveTiramisu() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + geofenceInternalWithMockContext.enable(null) + geofenceInternalWithMockContext.disable() + geofenceInternalWithMockContext.enable(null) - verify( - mockContext, timeout(100).times(2) - ).registerReceiver(any(), any()) + verify( + mockContext, timeout(100).times(2) + ).registerReceiver( + any(), + any(), + eq(Context.RECEIVER_EXPORTED) + ) + } + } + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.S_V2) + fun testDisableBelowTiramisu() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + geofenceInternalWithMockContext.enable(null) + geofenceInternalWithMockContext.disable() + geofenceInternalWithMockContext.enable(null) + + verify( + mockContext, timeout(100).times(2) + ).registerReceiver(any(), any()) + } } @Test diff --git a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/FetchGeofencesActionTest.kt b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/FetchGeofencesActionTest.kt index b0da646c..790b6ced 100644 --- a/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/FetchGeofencesActionTest.kt +++ b/mobile-engage/src/androidTest/java/com/emarsys/mobileengage/geofence/FetchGeofencesActionTest.kt @@ -8,8 +8,8 @@ import com.emarsys.mobileengage.util.waitForTask import com.emarsys.testUtil.AnnotationSpec -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify +import io.mockk.mockk +import io.mockk.verify class FetchGeofencesActionTest : AnnotationSpec() { @@ -20,8 +20,8 @@ class FetchGeofencesActionTest : AnnotationSpec() { @Before fun setUp() { - mockGeofenceInternal = mock() - mockActivity = mock() + mockGeofenceInternal = mockk(relaxed = true) + mockActivity = mockk(relaxed = true) setupMobileEngageComponent(FakeMobileEngageDependencyContainer()) @@ -39,7 +39,7 @@ class FetchGeofencesActionTest : AnnotationSpec() { fetchGeofencesAction.execute(mockActivity) waitForTask() - verify(mockGeofenceInternal).fetchGeofences(null) + verify { mockGeofenceInternal.fetchGeofences(null) } } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt index c00bfee9..c826c99c 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt @@ -149,10 +149,18 @@ class DefaultGeofenceInternal( private fun registerBroadcastReceiver() { if (!receiverRegistered) { concurrentHandlerHolder.postOnMain { - context.registerReceiver( - geofenceBroadcastReceiver, - IntentFilter("com.emarsys.sdk.GEOFENCE_ACTION") - ) + if (AndroidVersionUtils.isTiramisuOrAbove) { + context.registerReceiver( + geofenceBroadcastReceiver, + IntentFilter("com.emarsys.sdk.GEOFENCE_ACTION"), + Context.RECEIVER_EXPORTED + ) + } else { + context.registerReceiver( + geofenceBroadcastReceiver, + IntentFilter("com.emarsys.sdk.GEOFENCE_ACTION"), + ) + } } receiverRegistered = true } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt index dc70d19b..b0fa1405 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt @@ -10,19 +10,20 @@ import com.emarsys.core.util.AndroidVersionUtils class GeofencePendingIntentProvider(private val context: Context) { fun providePendingIntent(): PendingIntent { val intent = Intent("com.emarsys.sdk.GEOFENCE_ACTION") - if (AndroidVersionUtils.isBelowS) { + intent.setPackage(context.packageName) + if (AndroidVersionUtils.isUOrAbove) { return PendingIntent.getBroadcast( context, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) } else { return PendingIntent.getBroadcast( context, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + PendingIntent.FLAG_UPDATE_CURRENT ) } }