diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..977ded4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: CI - Tests +on: + push: + branches-ignore: + - 'main' + - 'releases/**' +jobs: + Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Make gradlew executable + run: chmod +x ./gradlew + - name: Run Tests + run: ./gradlew test + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: Test Results + path: app/build/test-results/testDebugUnitTest/TEST-*.xml # Path to test results + reporter: java-junit # Format of test results + fail-on-error: true \ No newline at end of file diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c9d1714..3add3e3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,10 @@ # Release Notes +### 6.10.1 +* Update Android SDK to v6.10.1 +* Added unit tests. +* Added github workflows. + ### 6.8.2 * Update Android SDK to v6.8.2 diff --git a/Readme.md b/Readme.md index 5eb1ef6..034a0a8 100644 --- a/Readme.md +++ b/Readme.md @@ -21,7 +21,7 @@ You can track installs, updates and sessions and also track additional in-app ev --- -Built with AppsFlyer Android SDK `v6.8.2` +Built with AppsFlyer Android SDK `v6.10.1` ## Table of content diff --git a/app/build.gradle b/app/build.gradle index 0a095e4..01ce7b8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,15 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 26 + compileSdkVersion 32 defaultConfig { minSdkVersion 14 - targetSdkVersion 26 + targetSdkVersion 32 versionCode 1 versionName "1.0" + testApplicationId "com.example.test" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -19,15 +21,42 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + testOptions { + unitTests { + all { + testLogging { + exceptionFormat = "full" + events "PASSED", "FAILED", "SKIPPED" + } + forkEvery 1 + } + includeAndroidResources = true + returnDefaultValues = true + } + } } dependencies { - implementation 'com.appsflyer:af-android-sdk:6.8.2' + implementation 'androidx.test.ext:junit:1.1.5' + implementation 'com.appsflyer:af-android-sdk:6.10.1' compileOnly 'com.android.installreferrer:installreferrer:2.1' compileOnly 'com.segment.analytics.android:analytics:4.+' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'androidx.test:core:1.4.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.9.2' + testImplementation 'com.android.installreferrer:installreferrer:2.1' + testImplementation 'com.segment.analytics.android:analytics:4.+' + testImplementation 'org.mockito:mockito-core:4.2.0' testImplementation 'com.segment.analytics.android:analytics-tests:4.+' } -apply from: file('publish.gradle') \ No newline at end of file +tasks.withType(Test) { + testLogging { + exceptionFormat "full" + events "started", "skipped", "passed", "failed" + showStandardStreams true + } +} + +apply from:file("publish.gradle") \ No newline at end of file diff --git a/app/publish.gradle b/app/publish.gradle index fb33ec9..961640c 100644 --- a/app/publish.gradle +++ b/app/publish.gradle @@ -1,63 +1,129 @@ -apply plugin: 'maven' +apply plugin: 'maven-publish' apply plugin: 'signing' -afterEvaluate { project -> - uploadArchives { - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - pom.groupId = GROUP - pom.artifactId = POM_ARTIFACT_ID - pom.version = VERSION_NAME - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) - } - pom.project { - name POM_NAME - packaging POM_PACKAGING - description POM_DESCRIPTION - url POM_URL - scm { - url POM_SCM_URL - connection POM_SCM_CONNECTION - developerConnection POM_SCM_DEV_CONNECTION - } - licenses { - license { - name POM_LICENCE_NAME - url POM_LICENCE_URL - distribution POM_LICENCE_DIST - } - } - developers { - developer { - id POM_DEVELOPER_ID - name POM_DEVELOPER_NAME +def isReleaseBuild() { + return !VERSION_NAME.contains("SNAPSHOT") +} + +def getReleaseRepositoryUrl() { + return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL + : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" +} + +def getSnapshotRepositoryUrl() { + return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL + : "https://oss.sonatype.org/content/repositories/snapshots/" +} + +task androidJavadocs(type: Javadoc) { + exclude "**/*.orig" // exclude files created by source control + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + failOnError false +} + +task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { + archiveClassifier.set("javadoc") + + from androidJavadocs.destinationDir +} + +task androidSourcesJar(type: Jar) { + archiveClassifier.set("sources") + + from android.sourceSets.main.java.source +} + +def logger(log) { + println log +} + +def configurePom(pom) { + logger("configurePom") + pom.name = POM_NAME + pom.packaging = POM_PACKAGING + pom.description = POM_DESCRIPTION + pom.url = POM_URL + + pom.scm { + url = POM_SCM_URL + connection = POM_SCM_CONNECTION + developerConnection = POM_SCM_DEV_CONNECTION + } + + pom.licenses { + license { + name = POM_LICENCE_NAME + url = POM_LICENCE_URL + distribution = POM_LICENCE_DIST + } + } + + pom.developers { + developer { + id = POM_DEVELOPER_ID + name = POM_DEVELOPER_NAME + } + } +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + logger("release") + // The coordinates of the library, being set from variables that + // we'll set up in a moment + groupId GROUP + artifactId POM_ARTIFACT_ID + version VERSION_NAME + + // Two artifacts, the `aar` and the sources + // artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") + artifact bundleReleaseAar + artifact androidSourcesJar + artifact androidJavadocsJar + + // Self-explanatory metadata for the most part + pom { + configurePom(pom) + // A slight fix so that the generated POM will include any transitive dependencies + // that the library builds upon + withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) } } } } } - } - signing { - required { gradle.taskGraph.hasTask("uploadArchives") } - sign configurations.archives - } - task androidJavadocs(type: Javadoc) { - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - if (JavaVersion.current().isJava8Compatible()) { - allprojects { - tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') } + repositories { + maven { + name = "sonatype" + + // You only need this if you want to publish snapshots, otherwise just set the URL + // to the release repo directly + url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() + + credentials(PasswordCredentials) { + username = getRepositoryUsername() + password = getRepositoryPassword() + } } } } - task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { - archiveClassifier = 'javadoc' - from androidJavadocs.destinationDir - } - task androidSourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.sourceFiles - } - artifacts { archives androidJavadocsJar } -} \ No newline at end of file +} + +signing { + logger("signing") + sign publishing.publications +} + + +publish.dependsOn build +publishToMavenLocal.dependsOn build \ No newline at end of file diff --git a/app/src/androidTest/java/com/segment/analytics/android/integrations/appsflyer/ApplicationTest.java b/app/src/androidTest/java/com/segment/analytics/android/integrations/appsflyer/ApplicationTest.java index 56a28c8..5d4cb89 100644 --- a/app/src/androidTest/java/com/segment/analytics/android/integrations/appsflyer/ApplicationTest.java +++ b/app/src/androidTest/java/com/segment/analytics/android/integrations/appsflyer/ApplicationTest.java @@ -1,13 +1,13 @@ -package com.segment.analytics.android.integration.appsflyer; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file +//package com.segment.analytics.android.integration.appsflyer; +// +//import android.app.Application; +//import android.test.ApplicationTestCase; +// +///** +// * Testing Fundamentals +// */ +//public class ApplicationTest extends ApplicationTestCase { +// public ApplicationTest() { +// super(Application.class); +// } +//} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d9d6c4e..f49da06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ - - - + \ No newline at end of file diff --git a/app/src/main/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegration.java b/app/src/main/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegration.java index 22e10cf..145961b 100644 --- a/app/src/main/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegration.java +++ b/app/src/main/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegration.java @@ -80,6 +80,9 @@ public static void startAppsFlyer(@NonNull Context context){ public static final Factory FACTORY = new Integration.Factory() { @Override public Integration create(ValueMap settings, Analytics analytics) { + if(settings == null || analytics == null){ + return null; + } Logger logger = analytics.logger(APPSFLYER_KEY); AppsFlyerLib afLib = AppsFlyerLib.getInstance(); diff --git a/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationConversionListenerTests.java b/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationConversionListenerTests.java new file mode 100644 index 0000000..32f2378 --- /dev/null +++ b/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationConversionListenerTests.java @@ -0,0 +1,213 @@ +package com.segment.analytics.android.integrations.appsflyer; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.segment.analytics.Analytics; +import com.segment.analytics.Properties; +import com.segment.analytics.ValueMap; +import static org.mockito.Mockito.*; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class AppsflyerIntegrationConversionListenerTests { + + @Test + public void testAppsflyerIntegration_ConversionListener_ctor_happyFlow() { + Analytics analytics = mock(Analytics.class); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + Assert.assertEquals(conversionListener.analytics, analytics); + + reset(analytics); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_ctor_nullFlow() { + Analytics analytics = null; + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + Assert.assertEquals(conversionListener.analytics, analytics); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_onConversionDataSuccess_happyFlow() { + //I want just to check the conversionListener gets the map. + AppsflyerIntegration.conversionListener = mock(AppsflyerIntegration.ExternalAppsFlyerConversionListener.class); + Analytics analytics = mock(Analytics.class); + Map conversionData = new ValueMap(); + Application app = mock(Application.class); + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + when(analytics.getApplication()).thenReturn(app); + when(app.getApplicationContext()).thenReturn(context); + when(context.getSharedPreferences("appsflyer-segment-data",0)).thenReturn(sharedPreferences); + when(sharedPreferences.getBoolean("AF_onConversion_Data",false)).thenReturn(true); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + conversionListener.onConversionDataSuccess(conversionData); + + verify(AppsflyerIntegration.conversionListener).onConversionDataSuccess(conversionData); + + reset(AppsflyerIntegration.conversionListener,analytics,app,context,sharedPreferences); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_onAttributionFailure_happyFlow() { + AppsflyerIntegration.conversionListener = mock(AppsflyerIntegration.ExternalAppsFlyerConversionListener.class); + Analytics analytics = Mockito.mock(Analytics.class); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + String errorMsg = "error - test"; + + conversionListener.onAttributionFailure(errorMsg); + + verify(AppsflyerIntegration.conversionListener,times(1)).onAttributionFailure(errorMsg); + + reset(analytics,AppsflyerIntegration.conversionListener); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_onAttributionFailure_nullFlow() { + AppsflyerIntegration.conversionListener = mock(AppsflyerIntegration.ExternalAppsFlyerConversionListener.class); + Analytics analytics = Mockito.mock(Analytics.class); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + String errorMsg = null; + conversionListener.onAttributionFailure(errorMsg); + verify(AppsflyerIntegration.conversionListener,times(1)).onAttributionFailure(null); + + reset(analytics,AppsflyerIntegration.conversionListener); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_trackInstallAttributed_happyFlow() { + Analytics analytics =mock(Analytics.class); + Map attributionData = new HashMap<>(); + attributionData.put("media_source", "media_source_moris"); + attributionData.put("campaign", "campaign_moris"); + attributionData.put("adgroup", "adgroup_moris"); + + Map campaign = new ValueMap() + .putValue("source", attributionData.get("media_source")) + .putValue("name", attributionData.get("campaign")) + .putValue("ad_group", attributionData.get("adgroup")); + Properties properties = new Properties().putValue("provider", "AppsFlyer"); + properties.putAll(attributionData); + properties.remove("media_source"); + properties.remove("adgroup"); + properties.putValue("campaign", campaign); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + conversionListener.trackInstallAttributed(attributionData); + + verify(analytics,times(1)).track("Install Attributed", properties); + + reset(analytics); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_trackInstallAttributed_negativeFlow() { + Analytics analytics =mock(Analytics.class); + Map attributionData = new HashMap(); + Map campaign = new ValueMap() // + .putValue("source", "") + .putValue("name", "") + .putValue("ad_group", ""); + Properties properties = new Properties().putValue("provider", "AppsFlyer"); + properties.putAll(attributionData); + properties.remove("media_source"); + properties.remove("adgroup"); + properties.putValue("campaign", campaign); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + conversionListener.trackInstallAttributed(attributionData); + + verify(analytics,times(1)).track("Install Attributed", properties); + + reset(analytics); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_getFlag_happyFlow() throws Exception { + String key="key"; + Analytics analytics = mock(Analytics.class); + Application app = mock(Application.class); + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + when(analytics.getApplication()).thenReturn(app); + when(app.getApplicationContext()).thenReturn(context); + when(context.getSharedPreferences("appsflyer-segment-data",0)).thenReturn(sharedPreferences); + when(sharedPreferences.getBoolean(key,false)).thenReturn(true); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + boolean resBoolean = (Boolean) TestHelper.getPrivateMethodForObjectReadyToInvoke("getFlag",String.class).invoke(conversionListener,key); + + Assert.assertTrue(resBoolean); + + reset(analytics,app,context,sharedPreferences); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_setFlag_happyFlow() throws Exception { + String key="key"; + boolean value=true; + Analytics analytics = mock(Analytics.class); + Application app = mock(Application.class); + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class); + when(analytics.getApplication()).thenReturn(app); + when(app.getApplicationContext()).thenReturn(context); + when(context.getSharedPreferences("appsflyer-segment-data",0)).thenReturn(sharedPreferences); + when(sharedPreferences.edit()).thenReturn(editor); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + TestHelper.getPrivateMethodForObjectReadyToInvoke("setFlag",String.class,boolean.class).invoke(conversionListener,key,value); + + if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD){ + verify(editor,times(1)).apply(); + } + else{ + verify(editor,times(1)).commit(); + } + + reset(analytics,app,context,sharedPreferences); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_getContext_happyFlow() throws Exception { + Analytics analytics = mock(Analytics.class); + Application app = mock(Application.class); + Context context = mock(Context.class); + when(analytics.getApplication()).thenReturn(app); + when(app.getApplicationContext()).thenReturn(context); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + Context resContext = (Context) TestHelper.getPrivateMethodForObjectReadyToInvoke("getContext").invoke(conversionListener); + + Assert.assertEquals(resContext, context); + + reset(analytics,app,context); + } + + @Test + public void testAppsflyerIntegration_ConversionListener_getContext_nullFlow() throws Exception{ + Analytics analytics = mock(Analytics.class); + Application app = mock(Application.class); + Context context = null; + when(analytics.getApplication()).thenReturn(app); + when(app.getApplicationContext()).thenReturn(context); + AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); + + Context resContext = (Context) TestHelper.getPrivateMethodForObjectReadyToInvoke("getContext").invoke(conversionListener); + + Assert.assertEquals(resContext, context); + + reset(analytics,app); + } +} diff --git a/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationTests.java b/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationTests.java new file mode 100644 index 0000000..823b26b --- /dev/null +++ b/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationTests.java @@ -0,0 +1,221 @@ +package com.segment.analytics.android.integrations.appsflyer; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import android.app.Application; +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.appsflyer.AFInAppEventParameterName; +import com.appsflyer.AppsFlyerLib; +import com.segment.analytics.Analytics; +import com.segment.analytics.Properties; +import com.segment.analytics.Traits; +import com.segment.analytics.ValueMap; +import com.segment.analytics.integrations.IdentifyPayload; +import com.segment.analytics.integrations.Integration; +import com.segment.analytics.integrations.Logger; +import com.segment.analytics.integrations.TrackPayload; +import static org.mockito.Mockito.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class AppsflyerIntegrationTests { + private TestHelper testHelper = new TestHelper(); + + @Test + public void testAppsflyerIntegration_ctor_happyFlow() throws Exception { + Context context = mock(Context.class); + Logger logger = new Logger("test", Analytics.LogLevel.INFO); + AppsFlyerLib appsflyer = mock(AppsFlyerLib.class); + String appsflyerDevKey = "appsflyerDevKey"; + boolean isDebug = logger.logLevel != Analytics.LogLevel.NONE; + + AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(context,logger,appsflyer,appsflyerDevKey); + Assert.assertEquals(appsflyerIntegration.isDebug , isDebug); + Assert.assertEquals(appsflyerIntegration.appsFlyerDevKey, appsflyerDevKey); + Assert.assertEquals(appsflyerIntegration.appsflyer, appsflyer); + Assert.assertEquals(appsflyerIntegration.logger, logger); + Context contextInappsflyerIntegration = (Context) TestHelper.getPrivateFieldForObject("context",AppsflyerIntegration.class,appsflyerIntegration); + Assert.assertEquals(contextInappsflyerIntegration, context); +// checking the static clause + Assert.assertEquals(AppsflyerIntegration.MAPPER.get("revenue"), AFInAppEventParameterName.REVENUE); + Assert.assertEquals(AppsflyerIntegration.MAPPER.get("currency"), AFInAppEventParameterName.CURRENCY); + + reset(context,appsflyer); + } + + @Test + public void testAppsflyerIntegration_setManualMode_happyFlow() { + Assert.assertFalse(AppsflyerIntegration.manualMode); + AppsflyerIntegration.setManualMode(true); + Assert.assertTrue(AppsflyerIntegration.manualMode); + AppsflyerIntegration.setManualMode(false); + Assert.assertFalse(AppsflyerIntegration.manualMode); + } + + @Test + public void testAppsflyerIntegration_startAppsFlyer_happyFlow() { + AppsFlyerLib appsFlyerLib = testHelper.mockAppsflyerLib(); + Context context = mock(Context.class); + + AppsflyerIntegration.startAppsFlyer(context); + + verify(appsFlyerLib).start(context); + + reset(appsFlyerLib,context); + testHelper.closeMockAppsflyerLib(); + } + + @Test + public void testAppsflyerIntegration_startAppsFlyer_nilFlow() { + AppsFlyerLib appsFlyerLib = testHelper.mockAppsflyerLib(); + + AppsflyerIntegration.startAppsFlyer(null); + + verify(appsFlyerLib,never()).start(any()); + + reset(appsFlyerLib); + testHelper.closeMockAppsflyerLib(); + } + + @Test + public void testAppsflyerIntegration_FACTORYCreate_happyFlow() { + AppsFlyerLib appsFlyerLib = testHelper.mockAppsflyerLib(); + Analytics analytics = mock(Analytics.class); + ValueMap settings = new ValueMap(); + settings.put("appsFlyerDevKey" , "devKey"); + settings.put("trackAttributionData" , true); + Logger logger = new Logger("test", Analytics.LogLevel.INFO); + Mockito.when(analytics.logger("AppsFlyer")).thenReturn(logger); + Application app = mock(Application.class); + Mockito.when(analytics.getApplication()).thenReturn(app); + AppsflyerIntegration.deepLinkListener = mock(AppsflyerIntegration.ExternalDeepLinkListener.class); + + Integration integration= (Integration) AppsflyerIntegration.FACTORY.create(settings,analytics); + + verify(appsFlyerLib).setDebugLog(logger.logLevel!=Analytics.LogLevel.NONE); + ArgumentCaptor captorListener = ArgumentCaptor.forClass(AppsflyerIntegration.ConversionListener.class); + ArgumentCaptor captorDevKey = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captorContext = ArgumentCaptor.forClass(Context.class); + verify(appsFlyerLib).init(captorDevKey.capture(), captorListener.capture() , captorContext.capture()); + Assert.assertNotNull(captorListener.getValue()); + Assert.assertEquals(captorDevKey.getValue(), settings.getString("appsFlyerDevKey")); + Assert.assertEquals(captorContext.getValue(), app.getApplicationContext()); + verify(appsFlyerLib).subscribeForDeepLink(AppsflyerIntegration.deepLinkListener); + + reset(appsFlyerLib,analytics,app,AppsflyerIntegration.deepLinkListener); + testHelper.closeMockAppsflyerLib(); + } + + @Test + public void testAppsflyerIntegration_FACTORYCreate_nilFlow() { + Analytics analytics = null; + ValueMap settings = null; + + Integration integration= (Integration) AppsflyerIntegration.FACTORY.create(settings,analytics); + + Assert.assertNull(integration); + } + + @Test + public void testAppsflyerIntegration_FACTORYKEY_happyFlow() { + Assert.assertEquals(AppsflyerIntegration.FACTORY.key(),"AppsFlyer"); + } + + @Test + public void testAppsflyerIntegration_getUnderlyingInstance_happyFlow() { + AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); + Logger logger = new Logger("test", Analytics.LogLevel.INFO); + AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); + + Assert.assertEquals(appsflyerIntegration.getUnderlyingInstance(),appsFlyerLib); + + reset(appsFlyerLib); + } + + @Test + public void testAppsflyerIntegration_identify_happyFlow() throws Exception { + AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); + Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); + AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); + IdentifyPayload identifyPayload = mock(IdentifyPayload.class); + Traits traits = mock(Traits.class); + when(identifyPayload.userId()).thenReturn("moris"); + when(identifyPayload.traits()).thenReturn(traits); + when(traits.getString("currencyCode")).thenReturn("ILS"); + + appsflyerIntegration.identify(identifyPayload); + + verify(logger, never()).verbose(any()); + String customerUserIdInappsflyerIntegration = (String) TestHelper.getPrivateFieldForObject("customerUserId",AppsflyerIntegration.class,appsflyerIntegration); + Assert.assertEquals(customerUserIdInappsflyerIntegration, "moris"); + String currencyCodeInappsflyerIntegration = (String) TestHelper.getPrivateFieldForObject("currencyCode",AppsflyerIntegration.class,appsflyerIntegration); + Assert.assertEquals(currencyCodeInappsflyerIntegration, "ILS"); + + reset(appsFlyerLib,identifyPayload,traits); + } + + @Test + public void testAppsflyerIntegration_identify_nilflow() { + Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); + AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,null,null); + IdentifyPayload identifyPayload = mock(IdentifyPayload.class); + Traits traits = mock(Traits.class); + when(identifyPayload.traits()).thenReturn(traits); + + appsflyerIntegration.identify(identifyPayload); + + verify(logger, times(1)).verbose("couldn't update 'Identify' attributes"); + + reset(identifyPayload,traits); + } + + @Test + public void testAppsflyerIntegration_updateEndUserAttributes_happyflow() throws Exception { + AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); + Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); + AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); + Method updateEndUserAttributes = AppsflyerIntegration.class.getDeclaredMethod("updateEndUserAttributes"); + updateEndUserAttributes.setAccessible(true); + TestHelper.setPrivateFieldForObject("customerUserId",AppsflyerIntegration.class,appsflyerIntegration,String.class,"Moris"); + TestHelper.setPrivateFieldForObject("currencyCode",AppsflyerIntegration.class,appsflyerIntegration,String.class,"ILS"); + updateEndUserAttributes.invoke(appsflyerIntegration); + + verify(logger, times(1)).verbose("appsflyer.setCustomerUserId(%s)", "Moris"); + verify(logger, times(1)).verbose("appsflyer.setCurrencyCode(%s)", "ILS"); + verify(logger, times(1)).verbose("appsflyer.setDebugLog(%s)", true); + + reset(appsFlyerLib); + } + + @Test + public void testAppsflyerIntegration_track_happyflow() throws Exception { + AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); + Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); + AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); + TrackPayload trackPayload = mock(TrackPayload.class); + String event = "event"; + Properties properties= mock(Properties.class); + Map afProperties = mock(Map.class); + MockedStatic staticUtils = mockStatic(com.segment.analytics.internal.Utils.class); + when(trackPayload.event()).thenReturn(event); + when(trackPayload.properties()).thenReturn(properties); + staticUtils.when(()->com.segment.analytics.internal.Utils.transform(any(),any())).thenReturn(afProperties); + + appsflyerIntegration.track(trackPayload); + + Context contextInAppsflyerIntegration = (Context) TestHelper.getPrivateFieldForObject("context",AppsflyerIntegration.class,appsflyerIntegration); + verify(appsFlyerLib, times(1)).logEvent(contextInAppsflyerIntegration,event,afProperties); + verify(logger, times(1)).verbose("appsflyer.logEvent(context, %s, %s)", event, properties); + + reset(appsFlyerLib,trackPayload,properties,afProperties); + staticUtils.close(); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/ExampleUnitTest.java b/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/ExampleUnitTest.java deleted file mode 100644 index 210297a..0000000 --- a/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/ExampleUnitTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.segment.analytics.android.integration.appsflyer; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * To work on unit tests, switch the Test Artifact in the Build Variants view. - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/TestHelper.java b/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/TestHelper.java new file mode 100644 index 0000000..15f3ff0 --- /dev/null +++ b/app/src/test/java/com/segment/analytics/android/integrations/appsflyer/TestHelper.java @@ -0,0 +1,42 @@ +package com.segment.analytics.android.integrations.appsflyer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import com.appsflyer.AppsFlyerLib; + +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class TestHelper { + MockedStatic staticAppsFlyerLib; + public AppsFlyerLib mockAppsflyerLib(){ + this.staticAppsFlyerLib = mockStatic(AppsFlyerLib.class); + AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); + this.staticAppsFlyerLib.when(AppsFlyerLib::getInstance).thenReturn(appsFlyerLib); + return appsFlyerLib; + } + public void closeMockAppsflyerLib(){ + this.staticAppsFlyerLib.close(); + } + + public static Object getPrivateFieldForObject(String fieldName, Class classObject, Object objToGetValueFrom) throws Exception{ + Field field = classObject.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(classObject.cast(objToGetValueFrom)); + } + + public static void setPrivateFieldForObject(String fieldName, Class classObject, Object objToGetValueFrom, Class valueClass, Object value) throws Exception{ + Field field = classObject.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(objToGetValueFrom,valueClass.cast(value)); + } + + public static Method getPrivateMethodForObjectReadyToInvoke(String funcName,Class... parameterTypesForMethod) throws Exception{ + Method getFlagMethod = AppsflyerIntegration.ConversionListener.class.getDeclaredMethod(funcName,parameterTypesForMethod); + getFlagMethod.setAccessible(true); + return getFlagMethod; + } +} diff --git a/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..1f0955d --- /dev/null +++ b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/build.gradle b/build.gradle index 3bf9fda..77267c5 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.3' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.2" } } diff --git a/gradle.properties b/gradle.properties index a18c791..394d461 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,8 +19,8 @@ GROUP=com.appsflyer -VERSION_CODE=10 -VERSION_NAME=6.8.2 +VERSION_CODE=11 +VERSION_NAME=6.10.1 POM_ARTIFACT_ID=segment-android-integration POM_PACKAGING=aar @@ -40,4 +40,4 @@ POM_DEVELOPER_ID=appsflyer POM_DEVELOPER_NAME=AppsFlyer, Inc. android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b0ec43a..50ecda4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu May 30 11:45:15 IDT 2019 +#Tue Jan 10 15:43:55 IST 2023 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/segmenttestapp/build.gradle b/segmenttestapp/build.gradle index 0736c1d..5d50692 100644 --- a/segmenttestapp/build.gradle +++ b/segmenttestapp/build.gradle @@ -27,12 +27,7 @@ dependencies { implementation project(path: ':app') testImplementation 'junit:junit:4.12' implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.appsflyer:af-android-sdk:6.8.2' - //noinspection GradleDynamicVersion + implementation 'com.appsflyer:af-android-sdk:6.10.1' implementation 'com.segment.analytics.android:analytics:4.+' -// compile 'com.appsflyer:segment-android-integration:6.8.2' implementation 'com.android.installreferrer:installreferrer:2.1' - //compile project(':app') - // compile 'com.google.firebase:firebase-crash:9.4.0' -} -//apply plugin: 'com.google.gms.google-services' \ No newline at end of file +} \ No newline at end of file diff --git a/segmenttestapp/src/main/java/com/appsflyer/segment/app/SampleApplication.java b/segmenttestapp/src/main/java/com/appsflyer/segment/app/SampleApplication.java index 4fdc1f4..998dc15 100644 --- a/segmenttestapp/src/main/java/com/appsflyer/segment/app/SampleApplication.java +++ b/segmenttestapp/src/main/java/com/appsflyer/segment/app/SampleApplication.java @@ -15,7 +15,7 @@ public class SampleApplication extends Application { - static final String SEGMENT_WRITE_KEY = ""; + static final String SEGMENT_WRITE_KEY = ""; static final String TAG = "SEG_AF"; @Override public void onCreate() { diff --git a/segmenttestapp/src/main/res/layout-v11/activity_main.xml b/segmenttestapp/src/main/res/layout-v11/activity_main.xml index 35118c3..2b36d7b 100644 --- a/segmenttestapp/src/main/res/layout-v11/activity_main.xml +++ b/segmenttestapp/src/main/res/layout-v11/activity_main.xml @@ -1,5 +1,6 @@ + app:srcCompat="@drawable/segment_logo" /> + app:srcCompat="@drawable/segment_logo" />