diff --git a/build.gradle b/build.gradle index a222e170..c030adcc 100755 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,14 @@ buildscript { - ext.kotlin_version = '1.2.41' + ext.kotlin_version = '1.3.21' repositories { google() jcenter() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.1' + classpath 'com.android.tools.build:gradle:3.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'org.jacoco:org.jacoco.core:0.8.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c471b180..fc3741ee 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon May 07 20:12:54 CEST 2018 +#Thu Feb 28 08:10:16 CET 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/library/build.gradle b/library/build.gradle index 822fc324..82d21b3b 100755 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,12 +3,20 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'jacoco' group='com.github.Constructor-io' +jacoco { + toolVersion = '0.8.1' +} + +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true +} + android { - compileSdkVersion 27 - buildToolsVersion '27.0.3' + compileSdkVersion 28 dexOptions { maxProcessCount 4 @@ -17,7 +25,7 @@ android { defaultConfig { minSdkVersion 19 - targetSdkVersion 27 + targetSdkVersion 28 testInstrumentationRunner "${applicationId}.runner.RxAndroidJUnitRunner" versionCode 1 versionName '1.2.0' @@ -78,24 +86,89 @@ android { androidExtensions { experimental = true } + + libraryVariants.all { variant -> + variant.outputs.each { output -> + def lintTask = tasks["lint${variant.name.capitalize()}"] + output.assemble.dependsOn lintTask + } + } + } +task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + + def buildDir = "build/" + + def coverageSourceDirs = [ + "src/main/java" + ] + + def excludedClasses = [ + '**/databinding/**/*.*', + '**/android/databinding/*Binding.*', + '**/BR.*', + '**/R.*', + '**/R$*.*', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*_MembersInjector.*', + '**/Dagger*Component.*', + '**/Dagger*Component$Builder.*', + '**/*Module_*Factory.*', + '**/*Fragment*.*', + '**/*Activity*.*', + '**/*Adapter*.*', + '**/*ViewPager*.*', + '**/*ViewHolder*.*', + '**/*Module*.*' + ] + + def javaClasses = fileTree( + dir: "$buildDir/intermediates/classes/debug", + excludes: excludedClasses + ) + + def kotlinClasses = fileTree( + dir: "$buildDir/tmp/kotlin-classes/debug", + excludes: excludedClasses + ) + + classDirectories = files([ javaClasses ], [ kotlinClasses ]) + additionalSourceDirs = files(coverageSourceDirs) + sourceDirectories = files(coverageSourceDirs) + executionData = fileTree(dir: "$buildDir/jacoco", includes: [ + "testDebugUnitTest.exec" + ]) + + reports { + xml.enabled = true + html.enabled = true + } +} + +task getCoverage(type: Exec, dependsOn: 'jacocoUnitTestReport') { + group = "Reporting" + commandLine "open", "$buildDir/reports/jacoco/jacocoUnitTestReport/html/index.html" +} + + apply from: 'dependencies.gradle' dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' - implementation 'io.reactivex.rxjava2:rxjava:2.1.8' + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation 'io.reactivex.rxjava2:rxjava:2.1.13' implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0' - testImplementation 'io.mockk:mockk:1.7.14' + testImplementation 'io.mockk:mockk:1.9.kotlin12' testImplementation 'org.robolectric:robolectric:3.6.1' - testImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:3.14.1' implementation supportLibs implementation networkLibs implementation otherLibs - implementation 'com.android.support:support-v4:27.1.1' - implementation 'com.android.support:cardview-v7:27.1.1' + implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.android.support:cardview-v7:28.0.0' kapt annotationProcessorLibs kaptTest daggerCompiler kaptAndroidTest daggerCompiler diff --git a/library/dependencies.gradle b/library/dependencies.gradle index 41fd1ae9..574c9589 100755 --- a/library/dependencies.gradle +++ b/library/dependencies.gradle @@ -1,8 +1,8 @@ ext { versions = [ - support : "27.1.1", + support : "28.0.0", moshi : "1.5.0", - okHttp : "3.9.0", + okHttp : "3.14.1", retrofit: '2.3.0', dagger : '2.14.1', junit : '4.12', diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index a0e9167b..bf1aafbd 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -27,6 +27,10 @@ class Constants { const val GROUP_DISPLAY_NAME = "group[display_name]" const val USER_ID = "ui" const val TERM_UNKNOWN = "TERM_UNKNOWN" + const val PAGE = "page" + const val PER_PAGE = "num_results_per_page" + const val FILTER_GROUP_ID = "filters[group_id]" + const val FILTER_FACET = "filters[%s]" } object QueryValues { diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 4286a04c..94da4ee8 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -8,6 +8,7 @@ import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.Group import io.constructor.data.model.Suggestion +import io.constructor.data.model.search.SearchResponse import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule @@ -105,6 +106,25 @@ object ConstructorIo { return dataManager.getAutocompleteResults(query, params.toTypedArray()) } + fun getSearchResults(text: String, vararg facets: Pair>, page: Int? = null, perPage: Int? = null, groupId: Int? = null): Observable> { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + val encodedParams: ArrayList> = arrayListOf() + groupId?.let { encodedParams.add(Constants.QueryConstants.FILTER_GROUP_ID.urlEncode() to it.toString()) } + page?.let { + encodedParams.add(Constants.QueryConstants.PAGE.urlEncode() to page.toString().urlEncode()) + } + perPage?.let { + encodedParams.add(Constants.QueryConstants.PER_PAGE.urlEncode() to perPage.toString().urlEncode()) + } + encodedParams.add(Constants.QueryConstants.SESSION.urlEncode() to sessionId.toString().urlEncode()) + facets.forEach { facet -> + facet.second.forEach { + encodedParams.add(Constants.QueryConstants.FILTER_FACET.format(facet.first).urlEncode() to it.urlEncode()) + } + } + return dataManager.getSearchResults(text, encodedParams = encodedParams.toTypedArray()) + } + fun trackAutocompleteSelect(searchTerm: String, originalQuery: String, sectionName: String, group: Group? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() @@ -121,7 +141,7 @@ object ConstructorIo { }, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("trigger select error: ${t.message}") //To change body of created functions use File | Settings | File Templates. + e("Autocomplete Select event error: ${t.message}") })) } @@ -139,13 +159,14 @@ object ConstructorIo { }, { it.printStackTrace() errorCallback?.invoke(it) - e("trigger search error: ${it.message}") + e("Search Submit event error: ${it.message}") })) } fun trackConversion(itemName: String, customerId: String, revenue: Double?, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackConversion(searchTerm, itemName, customerId, "%.2f".format(revenue), + val revenueString = revenue?.let { "%.2f".format(revenue) } + disposable.add(dataManager.trackConversion(searchTerm, itemName, customerId, revenueString, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), Constants.QueryConstants.AUTOCOMPLETE_SECTION to (sectionName ?: preferenceHelper.defaultItemSection))).subscribeOn(Schedulers.io()) .subscribe({}, { t -> @@ -164,7 +185,7 @@ object ConstructorIo { .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Search result click event error: ${t.message}") + e("Search Result Click event error: ${t.message}") })) } @@ -176,7 +197,7 @@ object ConstructorIo { .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Conversion event error: ${t.message}") + e("Search Results Loaded event error: ${t.message}") })) } @@ -188,7 +209,21 @@ object ConstructorIo { .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Input focus event error: ${t.message}") + e("Input Focus event error: ${t.message}") + })) + } + + fun trackPurchase(clientIds: Array, revenue: Double?, sectionName: String? = null, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + val sectionNameParam = sectionName ?: preferenceHelper.defaultItemSection + val revenueString = revenue?.let { "%.2f".format(revenue) } + val params = mutableListOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionNameParam) + disposable.add(dataManager.trackPurchase(clientIds.toList(), revenueString, params.toTypedArray()).subscribeOn(Schedulers.io()) + .subscribe({}, { t -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Purchase event error: ${t.message}") })) } diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index d4582b01..6fe02d6a 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -1,6 +1,10 @@ package io.constructor.data +import com.squareup.moshi.Moshi +import io.constructor.BuildConfig import io.constructor.data.model.Suggestion +import io.constructor.data.model.search.SearchResponse +import io.constructor.data.remote.ApiPaths import io.constructor.data.remote.ConstructorApi import io.reactivex.Completable import io.reactivex.Observable @@ -9,9 +13,9 @@ import javax.inject.Singleton @Singleton class DataManager @Inject -constructor(private val constructorApi: ConstructorApi) { +constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi) { - fun getAutocompleteResults(text: String, params: Array> = arrayOf()): Observable?>> = constructorApi.getSuggestions(text, params.toMap()).map { + fun getAutocompleteResults(text: String, params: Array> = arrayOf()): Observable?>> = constructorApi.getAutocompleteResults(text, params.toMap()).map { if (!it.isError) { it.response()?.let { if (it.isSuccessful) { @@ -25,6 +29,30 @@ constructor(private val constructorApi: ConstructorApi) { } }.toObservable() + fun getSearchResults(text: String, encodedParams: Array> = arrayOf()): Observable> { + var dynamicUrl = BuildConfig.BASE_API_URL + "/${ApiPaths.URL_SEARCH.format(text)}" + encodedParams.forEachIndexed { index, pair -> + dynamicUrl += "${if (index != 0) "&" else "?" }${pair.first}=${pair.second}" + } + return constructorApi.getSearchResults(dynamicUrl).map { result -> + if (!result.isError) { + result.response()?.let { + if (it.isSuccessful){ + val adapter = moshi.adapter(SearchResponse::class.java) + val response = it.body()?.string() + val result = response?.let { adapter.fromJson(it) } + result?.rawData = response + ConstructorData.of(result!!) + } else { + ConstructorData.networkError(it.errorBody()?.string()) + } + } ?: ConstructorData.error(result.error()) + } else { + ConstructorData.error(result.error()) + } + }.toObservable() + } + fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { return constructorApi.trackAutocompleteSelect(term, params.toMap(), encodedParams.toMap()) } @@ -42,7 +70,7 @@ constructor(private val constructorApi: ConstructorApi) { } fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array> = arrayOf()): Completable { - return constructorApi.trackSearchResultTerm(term, itemName, customerId, params.toMap()) + return constructorApi.trackSearchResultClick(term, itemName, customerId, params.toMap()) } fun trackSearchResultsLoaded(term: String, resultCount: Int, params: Array>): Completable { @@ -53,8 +81,8 @@ constructor(private val constructorApi: ConstructorApi) { return constructorApi.trackInputFocus(term, params.toMap()) } - fun trackPurchase(params: Array>): Completable { - return constructorApi.trackPurchase(params.toMap()) + fun trackPurchase(customerIds: List, revenue: String? = null, params: Array>): Completable { + return constructorApi.trackPurchase(customerIds, revenue, params.toMap()) } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/search/FacetOption.kt b/library/src/main/java/io/constructor/data/model/search/FacetOption.kt new file mode 100644 index 00000000..3900e7c5 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/FacetOption.kt @@ -0,0 +1,3 @@ +package io.constructor.data.model.search + +data class FacetOption(val count: Int, val value: String?) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/search/Result.kt b/library/src/main/java/io/constructor/data/model/search/Result.kt new file mode 100644 index 00000000..364f7f26 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/Result.kt @@ -0,0 +1,5 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class Result(@Json(name = "data") val result: ResultData, @Json(name = "matched_terms") val matchedTerms: List?, val value: String) diff --git a/library/src/main/java/io/constructor/data/model/search/ResultData.kt b/library/src/main/java/io/constructor/data/model/search/ResultData.kt new file mode 100644 index 00000000..5595199e --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/ResultData.kt @@ -0,0 +1,12 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json +import io.constructor.data.model.Group + +data class ResultData(val description: String?, + val id: String, + @Json(name = "image_url") val imageUrl: String?, + val url: String?, + val facets: List?, + val groups: List?, + var metadata: Map?) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt b/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt new file mode 100644 index 00000000..d92e7986 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt @@ -0,0 +1,3 @@ +package io.constructor.data.model.search + +data class ResultFacet(val name: String, val values: List?) diff --git a/library/src/main/java/io/constructor/data/model/search/SearchData.kt b/library/src/main/java/io/constructor/data/model/search/SearchData.kt new file mode 100644 index 00000000..f40a9a98 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchData.kt @@ -0,0 +1,6 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + + +data class SearchData(val facets: List?, val groups: List?, val results: List?, @Json(name = "total_num_results") val resultCount: Int) diff --git a/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt b/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt new file mode 100644 index 00000000..fa861f52 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt @@ -0,0 +1,11 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class SearchFacet(val name: String, + @Json(name = "display_name") val displayName: String?, + val status: Map?, + val type: String?, + val min: Int?, + val max: Int?, + val options: List?) diff --git a/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt b/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt new file mode 100644 index 00000000..8dbe9fa2 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt @@ -0,0 +1,9 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class SearchGroup(@Json(name = "children") val children: List?, + @Json(name = "parents") val parents: List?, + val count: Int, + @Json(name = "display_name") val displayName: String, + @Json(name = "group_id") val groupId: Long) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/search/SearchResponse.kt b/library/src/main/java/io/constructor/data/model/search/SearchResponse.kt new file mode 100644 index 00000000..96d4ae86 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchResponse.kt @@ -0,0 +1,5 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class SearchResponse(@Json(name = "response") val searchData: SearchData, @Json(name = "result_id") val resultId: String, var rawData: String?) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt index 2264b226..a7c7b135 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -1,7 +1,7 @@ package io.constructor.data.remote object ApiPaths { - const val URL_GET_SUGGESTIONS = "autocomplete/{value}" + const val URL_AUTOCOMPLETE = "autocomplete/{value}" const val URL_AUTOCOMPLETE_SELECT_EVENT = "autocomplete/{term}/select" const val URL_SEARCH_SUBMIT_EVENT = "autocomplete/{term}/search" const val URL_SESSION_START_EVENT = "behavior" @@ -9,5 +9,5 @@ object ApiPaths { const val URL_SEARCH_RESULT_CLICK_EVENT = "autocomplete/{term}/click_through" const val URL_BEHAVIOR = "behavior" const val URL_PURCHASE = "autocomplete/TERM_UNKNOWN/purchase" - + const val URL_SEARCH = "search/%s" } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index 0a04d8ca..de40e272 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -1,18 +1,17 @@ package io.constructor.data.remote +import io.constructor.core.Constants import io.constructor.data.model.AutocompleteResult import io.reactivex.Completable import io.reactivex.Single +import okhttp3.ResponseBody import retrofit2.adapter.rxjava2.Result -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query -import retrofit2.http.QueryMap +import retrofit2.http.* interface ConstructorApi { - @GET(ApiPaths.URL_GET_SUGGESTIONS) - fun getSuggestions(@Path("value") value: String, @QueryMap data: Map): Single> + @GET(ApiPaths.URL_AUTOCOMPLETE) + fun getAutocompleteResults(@Path("value") value: String, @QueryMap data: Map): Single> @GET(ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT) fun trackAutocompleteSelect(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable @@ -27,7 +26,7 @@ interface ConstructorApi { fun trackConversion(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @Query("revenue") revenue: String?, @QueryMap params: Map): Completable @GET(ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT) - fun trackSearchResultTerm(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @QueryMap params: Map): Completable + fun trackSearchResultClick(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @QueryMap params: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) fun trackSearchResultsLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map): Completable @@ -36,5 +35,11 @@ interface ConstructorApi { fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map): Completable @GET(ApiPaths.URL_PURCHASE) - fun trackPurchase(@QueryMap params: Map): Completable + fun trackPurchase(@Query(Constants.QueryConstants.CUSTOMER_ID) customerIds: List, + @Query("revenue") revenue: String?, + @QueryMap params: Map): Completable + + @GET + fun getSearchResults(@Url searchUrl: String): Single> + } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/features/base/BaseActivity.kt b/library/src/main/java/io/constructor/features/base/BaseActivity.kt index 0677d23d..e98c0088 100755 --- a/library/src/main/java/io/constructor/features/base/BaseActivity.kt +++ b/library/src/main/java/io/constructor/features/base/BaseActivity.kt @@ -13,15 +13,6 @@ import io.constructor.injection.module.ActivityModule import io.constructor.util.d import java.util.concurrent.atomic.AtomicLong -/** - * Abstract activity that every other Activity in this application must implement. It provides the - * following functionality: - * - Handles creation of Dagger components and makes sure that instances of - * ConfigPersistentComponent are kept across configuration changes. - * - Set up and handles a GoogleApiClient instance that can be used to access the Google sign in - * api. - * - Handles signing out when an authentication error event is received. - */ abstract class BaseActivity : AppCompatActivity() { private var activityComponent: ActivityComponent? = null @@ -48,7 +39,7 @@ abstract class BaseActivity : AppCompatActivity() { componentsArray.put(activityId, configPersistentComponent) } else { d("Reusing ConfigPersistentComponent id=${activityId}") - configPersistentComponent = componentsArray.get(activityId) + configPersistentComponent = componentsArray.get(activityId)!! } activityComponent = configPersistentComponent.activityComponent(ActivityModule(this)) activityComponent?.inject(this) diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index 1d8982b2..66277685 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -273,9 +273,9 @@ class ConstructorIoTest { @Test fun trackPurchase() { every { pref.defaultItemSection } returns "Products" - every { data.trackPurchase(any()) } returns Completable.complete() - constructorIo.trackPurchase(arrayOf("id1")) - verify(exactly = 1) { data.trackPurchase(any()) } + every { data.trackPurchase(any(), any(), any()) } returns Completable.complete() + constructorIo.trackPurchase(arrayOf("id1"), 12.99) + verify(exactly = 1) { data.trackPurchase(any(), any(), any()) } } } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt new file mode 100755 index 00000000..76524626 --- /dev/null +++ b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt @@ -0,0 +1,513 @@ +package io.constructor.data + +import android.content.Context +import com.squareup.moshi.KotlinJsonAdapterFactory +import com.squareup.moshi.Moshi +import io.constructor.core.Constants +import io.constructor.data.interceptor.TokenInterceptor +import io.constructor.data.local.PreferencesHelper +import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.remote.ApiPaths +import io.constructor.data.remote.ConstructorApi +import io.constructor.util.RxSchedulersOverrideRule +import io.constructor.util.TestDataLoader +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.moshi.MoshiConverterFactory +import java.net.SocketTimeoutException +import java.util.concurrent.TimeUnit + + + + +class DataManagerHttpTest { + + @Rule + @JvmField val overrideSchedulersRule = RxSchedulersOverrideRule() + + private lateinit var constructorApi: ConstructorApi + + private val ctx = mockk() + private val pref = mockk() + private val configMemoryHolder = mockk() + + private lateinit var dataManager: DataManager + + private lateinit var mockServer: MockWebServer + + @Before + fun setup() { + every { pref.token } returns "123" + every { pref.id } returns "1" + every { configMemoryHolder.testCellParams = any() } just Runs + every { configMemoryHolder.userId } returns "id1" + every { configMemoryHolder.testCellParams } returns emptyList() + mockServer = MockWebServer() + mockServer.start() + val basePath = mockServer.url("" ) + + val client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)) + .addInterceptor { chain -> + var request = chain.request() + if (!request.url().toString().startsWith(basePath.toString())) { + val requestUrl = request.url() + val newRequestUrl = HttpUrl.Builder().scheme(basePath.scheme()) + .encodedQuery(requestUrl.encodedQuery()) + .host(basePath.host()) + .port(basePath.port()) + .encodedPath(requestUrl.encodedPath()).build() + request = request.newBuilder() + .url(newRequestUrl) + .build() + } + chain.proceed(request) + } + .readTimeout(4, TimeUnit.SECONDS).build() + + + val moshi = Moshi + .Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + // Get an instance of Retrofit + val retrofit = Retrofit.Builder() + .baseUrl(basePath.toString()) + .client(client) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + + constructorApi = retrofit.create(ConstructorApi::class.java) + dataManager = DataManager(constructorApi, moshi) + } + + @Test + fun getAutocompleteResults() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.get()!!.isNotEmpty() && it.get()!!.size == 5 + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsBadServerResponse() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.networkError + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsTimeoutException() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response.json")) + mockResponse.throttleBody(128, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.isError + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsUnexpectedDataResponse() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_with_unexpected_data.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.get()!!.isNotEmpty() && it.get()!!.size == 5 + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsEmptyResponse() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_empty.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.isEmpty + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackAutocompleteSelect() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackAutocompleteSelect("titanic").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackAutocompleteSelect500() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackAutocompleteSelect("titanic").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackAutocompleteTimeout() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackAutocompleteSelect("titanic").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSearchSubmit() { + val path = "/" + ApiPaths.URL_SEARCH_SUBMIT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchSubmit("titanic").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSearchSubmit500() { + val path = "/" + ApiPaths.URL_SEARCH_SUBMIT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchSubmit("titanic").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSearchSubmitTimeout() { + val path = "/" + ApiPaths.URL_SEARCH_SUBMIT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchSubmit("titanic").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSessionStart() { + val path = "/" + ApiPaths.URL_SESSION_START_EVENT + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to "1")).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("s=1")) + } + + @Test + fun trackSessionStart500() { + val path = "/" + ApiPaths.URL_SESSION_START_EVENT + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to "1")).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("s=1")) + } + + @Test + fun trackSessionStartTimeout() { + val path = "/" + ApiPaths.URL_SESSION_START_EVENT + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to "1")).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("s=1")) + } + + @Test + fun trackConversion() { + val path = "/" + ApiPaths.URL_CONVERSION_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackConversion("titanic", "ship", "cid").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackConversion500() { + val path = "/" + ApiPaths.URL_CONVERSION_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackConversion("titanic", "ship", "cid").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackConversionTimeout() { + val path = "/" + ApiPaths.URL_CONVERSION_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackConversion("titanic", "ship", "cid").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultClick() { + val path = "/" + ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultClick("ship", "cid", "titanic").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultClick500() { + val path = "/" + ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultClick("ship", "cid", "titanic").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultClickTimeout() { + val path = "/" + ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultClick("ship", "cid", "titanic").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultLoaded() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultsLoaded("titanic", 10, arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_SEARCH_RESULTS}")) + } + + @Test + fun trackSearchResultLoaded500() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultsLoaded("titanic", 10, arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_SEARCH_RESULTS}")) + } + + @Test + fun trackSearchResultLoadedTimeout() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultsLoaded("titanic", 10, arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_SEARCH_RESULTS}")) + } + + @Test + fun trackInputFocus() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackInputFocus("titanic", arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_INPUT_FOCUS}")) + } + + @Test + fun trackInputFocus500() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackInputFocus("titanic", arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_INPUT_FOCUS}")) + } + + @Test + fun trackInputFocusTimeout() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackInputFocus("titanic", arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_INPUT_FOCUS}")) + } + + @Test + fun trackPurchase() { + val path = "/" + ApiPaths.URL_PURCHASE + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackPurchase(listOf("1", "2"), "12.99", arrayOf()).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=1")) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=2")) + } + + @Test + fun trackPurchase500() { + val path = "/" + ApiPaths.URL_PURCHASE + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackPurchase(listOf("1", "2"), "12.99", arrayOf()).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=1")) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=2")) + } + + @Test + fun trackPurchaseTimeout() { + val path = "/" + ApiPaths.URL_PURCHASE + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackPurchase(listOf("1", "2"), "12.99", arrayOf()).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=1")) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=2")) + } + + @Test + fun getSearchResult() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.results!!.size == 20 + } + } + + @Test + fun getSearchResultsBadServerResponse() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.networkError + } + } + + @Test + fun getSearchResultsTimeoutException() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response.json")) + mockResponse.throttleBody(128, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.isError + } + } + + @Test + fun getSearchUnexpectedDataResponse() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_unexpected_data.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.resultCount == 23 + } + } + + @Test + fun getSearchResultsEmptyResponse() { + val path = "/" + ApiPaths.URL_SEARCH.format("corn") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_empty.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.results!!.isEmpty() + } + } + +} \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index 2ebfad37..115afe88 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -1,5 +1,7 @@ package io.constructor.data +import com.squareup.moshi.KotlinJsonAdapterFactory +import com.squareup.moshi.Moshi import io.constructor.data.model.AutocompleteResult import io.constructor.data.remote.ConstructorApi import io.constructor.util.RxSchedulersOverrideRule @@ -23,11 +25,16 @@ class DataManagerTest { private var constructorApi = mockk() - private var dataManager = DataManager(constructorApi) + private var moshi = Moshi + .Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + private var dataManager = DataManager(constructorApi, moshi) @Test - fun getSuggestions() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) + fun getAutocompleteResults() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -35,8 +42,8 @@ class DataManagerTest { } @Test - fun getSuggestionsBadServerResponse() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) + fun getAutocompleteResultsBadServerResponse() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.networkError @@ -44,8 +51,8 @@ class DataManagerTest { } @Test - fun getSuggestionsException() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.error(Exception())) + fun getAutocompleteResultsException() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.error(Exception())) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.isError @@ -53,8 +60,8 @@ class DataManagerTest { } @Test - fun getSuggestionsUnexpectedDataResponse() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) + fun getAutocompleteResultsUnexpectedDataResponse() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -62,8 +69,8 @@ class DataManagerTest { } @Test - fun getSuggestionsEmptyResponse() { - every { constructorApi.getSuggestions("titanic", any() + fun getAutocompleteResultsEmptyResponse() { + every { constructorApi.getAutocompleteResults("titanic", any() ) } returns Single.just(Result.response(Response.success(TestDataLoader.loadEmptyResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { @@ -141,17 +148,17 @@ class DataManagerTest { @Test fun trackSearchResultClick() { - every { constructorApi.trackSearchResultTerm(any(), any(), any(), any()) } returns Completable.complete() + every { constructorApi.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.complete() dataManager.trackSearchResultClick("term", "id1", "term1") - verify(exactly = 1) { constructorApi.trackSearchResultTerm(any(), any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchResultClick(any(), any(), any(), any())} } @Test fun trackSearchResultClickError() { - every { constructorApi.trackSearchResultTerm(any(), any(), any(), any()) } returns Completable.error(Exception()) + every { constructorApi.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.error(Exception()) val observer = dataManager.trackSearchResultClick("term", "1", "term1").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearchResultTerm(any(), any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchResultClick(any(), any(), any(), any())} } @Test @@ -186,9 +193,37 @@ class DataManagerTest { @Test fun trackPurchase() { - every { constructorApi.trackPurchase(any()) } returns Completable.complete() - dataManager.trackPurchase(arrayOf()) - verify(exactly = 1) { constructorApi.trackPurchase(any()) } + every { constructorApi.trackPurchase(any(), any(), any()) } returns Completable.complete() + dataManager.trackPurchase(listOf(), "12.99", arrayOf()) + verify(exactly = 1) { constructorApi.trackPurchase(any(), any(), any()) } + } + + @Test + fun getSearchResults() { + val rb = ResponseBody.create(MediaType.get("application/json"), TestDataLoader.loadAsString("search_response.json")) + every { constructorApi.getSearchResults(any()) } returns Single.just(Result.response(Response.success(rb))) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.resultCount == 23 + } + } + + @Test + fun getSearchResultsBadServerResponse() { + every { constructorApi.getSearchResults("https://ac.cnstrc.com/search/corn") } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.networkError + } + } + + @Test + fun getSearchResultsException() { + every { constructorApi.getSearchResults("https://ac.cnstrc.com/search/corn") } returns Single.just(Result.error(Exception())) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.isError + } } } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/util/TestDataLoader.kt b/library/src/test/java/io/constructor/util/TestDataLoader.kt index 2794d7ce..684130c0 100755 --- a/library/src/test/java/io/constructor/util/TestDataLoader.kt +++ b/library/src/test/java/io/constructor/util/TestDataLoader.kt @@ -3,19 +3,21 @@ package io.constructor.util import com.squareup.moshi.KotlinJsonAdapterFactory import com.squareup.moshi.Moshi import io.constructor.data.model.AutocompleteResult +import io.constructor.data.model.search.SearchResponse import okio.Buffer import java.io.File import java.io.FileInputStream import java.io.IOException +import java.nio.charset.Charset object TestDataLoader { - fun loadResponse() : AutocompleteResult? = loadResult("response.json") + fun loadResponse() : AutocompleteResult? = loadResult("autocomplete_response.json") - fun loadResponseWithUnexpectedData() : AutocompleteResult? = loadResult("response_with_unexpected_data.json") + fun loadResponseWithUnexpectedData() : AutocompleteResult? = loadResult("autocomplete_response_with_unexpected_data.json") - fun loadEmptyResponse() : AutocompleteResult? = loadResult("empty_response.json") + fun loadEmptyResponse() : AutocompleteResult? = loadResult("autocomplete_response_empty.json") private fun loadResult(fileName: String): AutocompleteResult? { val file = File(TestDataLoader::class.java.classLoader.getResource(fileName).path) @@ -30,4 +32,27 @@ object TestDataLoader { return result } + private fun convertToSearchResult(stringResponse: String): SearchResponse? { + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val jsonAdapter = moshi.adapter(SearchResponse::class.java) + var result: SearchResponse? = null + try { + result = jsonAdapter.fromJson(stringResponse) + } catch (e: IOException) { + e.printStackTrace() + } + return result + } + + fun loadAsString(fileName: String): String { + var result = "" + try { + result = File(TestDataLoader::class.java.classLoader.getResource(fileName).path).inputStream().readBytes().toString(Charset.defaultCharset()) + } catch (e: IOException) { + e.printStackTrace() + } + return result + } + + } diff --git a/library/src/test/resources/response.json b/library/src/test/resources/autocomplete_response.json similarity index 100% rename from library/src/test/resources/response.json rename to library/src/test/resources/autocomplete_response.json diff --git a/library/src/test/resources/empty_response.json b/library/src/test/resources/autocomplete_response_empty.json similarity index 100% rename from library/src/test/resources/empty_response.json rename to library/src/test/resources/autocomplete_response_empty.json diff --git a/library/src/test/resources/response_with_unexpected_data.json b/library/src/test/resources/autocomplete_response_with_unexpected_data.json similarity index 100% rename from library/src/test/resources/response_with_unexpected_data.json rename to library/src/test/resources/autocomplete_response_with_unexpected_data.json diff --git a/library/src/test/resources/search_response.json b/library/src/test/resources/search_response.json new file mode 100755 index 00000000..1cce0cbe --- /dev/null +++ b/library/src/test/resources/search_response.json @@ -0,0 +1,598 @@ +{ + "request": { + "ef-11": "22", + "ef-ab": "cd", + "fmt_options": { + "groups_max_depth": 1, + "groups_start": "current" + }, + "num_results_per_page": 20, + "page": 1, + "section": "Products", + "sort_by": "relevance", + "sort_order": "descending", + "term": "corn" + }, + "response": { + "facets": [], + "groups": [ + { + "children": [], + "count": 9, + "display_name": "Horror", + "group_id": "27", + "parents": [] + }, + { + "children": [], + "count": 7, + "display_name": "Thriller", + "group_id": "53", + "parents": [] + }, + { + "children": [], + "count": 4, + "display_name": "Drama", + "group_id": "18", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Documentary", + "group_id": "99", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Mystery", + "group_id": "9648", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Fantasy", + "group_id": "14", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Animation", + "group_id": "16", + "parents": [] + }, + { + "children": [], + "count": 6, + "display_name": "Comedy", + "group_id": "35", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Crime", + "group_id": "80", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Action", + "group_id": "28", + "parents": [] + }, + { + "children": [], + "count": 2, + "display_name": "Romance", + "group_id": "10749", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Science Fiction", + "group_id": "878", + "parents": [] + } + ], + "results": [ + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iv: the gathering", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/tRjeV9AZgCXGTqyvlp7Ui55Yb3l.jpg", + "url": "https://www.top250.tv/movies/25750" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn IV: The Gathering" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + }, + { + "display_name": "Fantasy", + "group_id": "14", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zH77CDSRPeYfZZJyyKSt84j62m8.jpg", + "url": "https://www.top250.tv/movies/10823" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn ii: the final sacrifice", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/lqFb8Mnx9tFPUevnfbz9o2adLFw.jpg", + "url": "https://www.top250.tv/movies/25748" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn II: The Final Sacrifice" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iii: urban harvest", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/Ajp5lVNAW0Kfi3uUlCCpIri28B8.jpg", + "url": "https://www.top250.tv/movies/25749" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn III: Urban Harvest" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the corn is green", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zBN62KUP4WGys96vmJUFRKc43B9.jpg", + "url": "https://www.top250.tv/movies/43492" + }, + "matched_terms": [ + "corn" + ], + "value": "The Corn Is Green" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "king corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pvqjdmu5IdzUPQgqwylpPrUSKSd.jpg", + "url": "https://www.top250.tv/movies/15281" + }, + "matched_terms": [ + "corn" + ], + "value": "King Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "corn island", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zM4ZZ7IpKQA266ynNWOvn3LfKE.jpg", + "url": "https://www.top250.tv/movies/282376" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn Island" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Mystery", + "group_id": "9648", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn 666: isaac's return", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/cAoLi0dxRZwA20LUzTTGN3Xn39Y.jpg", + "url": "https://www.top250.tv/movies/25752" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn 666: Isaac's Return" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn v: fields of terror", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/w3ZOi0jbHNEQ26MEt1X3XCJzBYe.jpg", + "url": "https://www.top250.tv/movies/25751" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn V: Fields of Terror" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: genesis", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/94Cc2YJMsCtezYRcmL1PyBNhE1y.jpg", + "url": "https://www.top250.tv/movies/70575" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Genesis" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: revelation", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/sL3ZaPFwgkfn9KuIsh861zsPX0Y.jpg", + "url": "https://www.top250.tv/movies/25753" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Revelation" + }, + { + "data": { + "groups": [ + { + "display_name": "Animation", + "group_id": "16", + "path": null, + "path_list": [] + } + ], + "id": "corn on the cop", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/1ifAyPrAJW4WDVqmvBmZo3hDhrD.jpg", + "url": "https://www.top250.tv/movies/234377" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn on the Cop" + }, + { + "data": { + "groups": [ + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "con air", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/yhaOQ7xXw0PLHLvg1w0M9zlPdg6.jpg", + "url": "https://www.top250.tv/movies/1701" + }, + "matched_terms": [ + "con" + ], + "value": "Con Air" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Science Fiction", + "group_id": "878", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + } + ], + "id": "def-con 4", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/wmCLwtkwzrrphDhR1smtFLZrQxG.jpg", + "url": "https://www.top250.tv/movies/42033" + }, + "matched_terms": [ + "con" + ], + "value": "Def-Con 4" + }, + { + "data": { + "groups": [ + { + "display_name": "Romance", + "group_id": "10749", + "path": null, + "path_list": [] + }, + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + }, + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the con", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/fDcPgFzVioueAcp13nLw8TGiVOC.jpg", + "url": "https://www.top250.tv/movies/131729" + }, + "matched_terms": [ + "con" + ], + "value": "The Con" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "comic-con episode iv: a fan's hope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/axltQJKHxolLfbGwuTKZZlLbBsZ.jpg", + "url": "https://www.top250.tv/movies/91356" + }, + "matched_terms": [ + "con" + ], + "value": "Comic-Con Episode IV: A Fan's Hope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "chronic-con, episode 420: a new dope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/16pPnt4ce0i3zU7QOoHDD4JN9Oe.jpg", + "url": "https://www.top250.tv/movies/347528" + }, + "matched_terms": [ + "con" + ], + "value": "Chronic-Con, Episode 420: A New Dope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "vaya con dios", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pIV1xgavkLYL33agVmRxpwq9CE4.jpg", + "url": "https://www.top250.tv/movies/6318" + }, + "matched_terms": [ + "con" + ], + "value": "Vaya con Dios" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "the con artists", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/rn2xTdw2pyRTcjcwHy5yEfxkfyQ.jpg", + "url": "https://www.top250.tv/movies/300433" + }, + "matched_terms": [ + "con" + ], + "value": "The Con Artists" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "tempo instabile con probabili schiarite", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/iaOhlyHXqAIhsoGXqD9S4I066zo.jpg", + "url": "https://www.top250.tv/movies/333888" + }, + "matched_terms": [ + "con" + ], + "value": "Tempo instabile con probabili schiarite" + } + ], + "sort_options": [], + "total_num_results": 23 + }, + "result_id": "5b19c365-1c85-4c2d-b627-0509d60ce2d5" +} diff --git a/library/src/test/resources/search_response_empty.json b/library/src/test/resources/search_response_empty.json new file mode 100644 index 00000000..5f82e2cc --- /dev/null +++ b/library/src/test/resources/search_response_empty.json @@ -0,0 +1,24 @@ +{ + "request": { + "ef-11": "22", + "ef-ab": "cd", + "fmt_options": { + "groups_max_depth": 1, + "groups_start": "current" + }, + "num_results_per_page": 20, + "page": 1, + "section": "Products", + "sort_by": "relevance", + "sort_order": "descending", + "term": "cornucopiasofcorndogs" + }, + "response": { + "facets": [], + "groups": [], + "results": [], + "sort_options": [], + "total_num_results": 0 + }, + "result_id": "2ea93527-91d6-4dfa-86b0-6a0e8158bfd1" +} diff --git a/library/src/test/resources/search_response_unexpected_data.json b/library/src/test/resources/search_response_unexpected_data.json new file mode 100755 index 00000000..7275c3f9 --- /dev/null +++ b/library/src/test/resources/search_response_unexpected_data.json @@ -0,0 +1,599 @@ +{ + "request": { + "ef-11": "22", + "ef-ab": "cd", + "fmt_options": { + "groups_max_depth": 1, + "groups_start": "current" + }, + "num_results_per_page": 20, + "page": 1, + "section": "Products", + "sort_by": "relevance", + "sort_order": "descending", + "term": "corn" + }, + "response": { + "facets": [], + "groups": [ + { + "children": [], + "count": 9, + "display_name": "Horror", + "group_id": "27", + "parents": [] + }, + { + "children": [], + "count": 7, + "display_name": "Thriller", + "group_id": "53", + "parents": [], + "unknown_name": "New name" + }, + { + "children": [], + "count": 4, + "display_name": "Drama", + "group_id": "18", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Documentary", + "group_id": "99", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Mystery", + "group_id": "9648", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Fantasy", + "group_id": "14", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Animation", + "group_id": "16", + "parents": [] + }, + { + "children": [], + "count": 6, + "display_name": "Comedy", + "group_id": "35", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Crime", + "group_id": "80", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Action", + "group_id": "28", + "parents": [] + }, + { + "children": [], + "count": 2, + "display_name": "Romance", + "group_id": "10749", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Science Fiction", + "group_id": "878", + "parents": [] + } + ], + "results": [ + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iv: the gathering", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/tRjeV9AZgCXGTqyvlp7Ui55Yb3l.jpg", + "url": "https://www.top250.tv/movies/25750" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn IV: The Gathering" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + }, + { + "display_name": "Fantasy", + "group_id": "14", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zH77CDSRPeYfZZJyyKSt84j62m8.jpg", + "url": "https://www.top250.tv/movies/10823" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn ii: the final sacrifice", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/lqFb8Mnx9tFPUevnfbz9o2adLFw.jpg", + "url": "https://www.top250.tv/movies/25748" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn II: The Final Sacrifice" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iii: urban harvest", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/Ajp5lVNAW0Kfi3uUlCCpIri28B8.jpg", + "url": "https://www.top250.tv/movies/25749" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn III: Urban Harvest" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the corn is green", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zBN62KUP4WGys96vmJUFRKc43B9.jpg", + "url": "https://www.top250.tv/movies/43492" + }, + "matched_terms": [ + "corn" + ], + "value": "The Corn Is Green" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "king corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pvqjdmu5IdzUPQgqwylpPrUSKSd.jpg", + "url": "https://www.top250.tv/movies/15281" + }, + "matched_terms": [ + "corn" + ], + "value": "King Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "corn island", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zM4ZZ7IpKQA266ynNWOvn3LfKE.jpg", + "url": "https://www.top250.tv/movies/282376" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn Island" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Mystery", + "group_id": "9648", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn 666: isaac's return", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/cAoLi0dxRZwA20LUzTTGN3Xn39Y.jpg", + "url": "https://www.top250.tv/movies/25752" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn 666: Isaac's Return" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn v: fields of terror", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/w3ZOi0jbHNEQ26MEt1X3XCJzBYe.jpg", + "url": "https://www.top250.tv/movies/25751" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn V: Fields of Terror" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: genesis", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/94Cc2YJMsCtezYRcmL1PyBNhE1y.jpg", + "url": "https://www.top250.tv/movies/70575" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Genesis" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: revelation", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/sL3ZaPFwgkfn9KuIsh861zsPX0Y.jpg", + "url": "https://www.top250.tv/movies/25753" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Revelation" + }, + { + "data": { + "groups": [ + { + "display_name": "Animation", + "group_id": "16", + "path": null, + "path_list": [] + } + ], + "id": "corn on the cop", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/1ifAyPrAJW4WDVqmvBmZo3hDhrD.jpg", + "url": "https://www.top250.tv/movies/234377" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn on the Cop" + }, + { + "data": { + "groups": [ + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "con air", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/yhaOQ7xXw0PLHLvg1w0M9zlPdg6.jpg", + "url": "https://www.top250.tv/movies/1701" + }, + "matched_terms": [ + "con" + ], + "value": "Con Air" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Science Fiction", + "group_id": "878", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + } + ], + "id": "def-con 4", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/wmCLwtkwzrrphDhR1smtFLZrQxG.jpg", + "url": "https://www.top250.tv/movies/42033" + }, + "matched_terms": [ + "con" + ], + "value": "Def-Con 4" + }, + { + "data": { + "groups": [ + { + "display_name": "Romance", + "group_id": "10749", + "path": null, + "path_list": [] + }, + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + }, + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the con", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/fDcPgFzVioueAcp13nLw8TGiVOC.jpg", + "url": "https://www.top250.tv/movies/131729" + }, + "matched_terms": [ + "con" + ], + "value": "The Con" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "comic-con episode iv: a fan's hope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/axltQJKHxolLfbGwuTKZZlLbBsZ.jpg", + "url": "https://www.top250.tv/movies/91356" + }, + "matched_terms": [ + "con" + ], + "value": "Comic-Con Episode IV: A Fan's Hope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "chronic-con, episode 420: a new dope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/16pPnt4ce0i3zU7QOoHDD4JN9Oe.jpg", + "url": "https://www.top250.tv/movies/347528" + }, + "matched_terms": [ + "con" + ], + "value": "Chronic-Con, Episode 420: A New Dope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "vaya con dios", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pIV1xgavkLYL33agVmRxpwq9CE4.jpg", + "url": "https://www.top250.tv/movies/6318" + }, + "matched_terms": [ + "con" + ], + "value": "Vaya con Dios" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "the con artists", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/rn2xTdw2pyRTcjcwHy5yEfxkfyQ.jpg", + "url": "https://www.top250.tv/movies/300433" + }, + "matched_terms": [ + "con" + ], + "value": "The Con Artists" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "tempo instabile con probabili schiarite", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/iaOhlyHXqAIhsoGXqD9S4I066zo.jpg", + "url": "https://www.top250.tv/movies/333888" + }, + "matched_terms": [ + "con" + ], + "value": "Tempo instabile con probabili schiarite" + } + ], + "sort_options": [], + "total_num_results": 23 + }, + "result_id": "5b19c365-1c85-4c2d-b627-0509d60ce2d5" +} diff --git a/sample/build.gradle b/sample/build.gradle index ad4401e4..c99a9db0 100755 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 27 + compileSdkVersion 28 defaultConfig { applicationId "io.constructor.sample" minSdkVersion 19 - targetSdkVersion 27 + targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -22,7 +22,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - buildToolsVersion '27.0.3' } configurations.all { @@ -36,9 +35,12 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation project(':library') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:recyclerview-v7:27.1.1' - implementation "com.android.support:cardview-v7:27.1.1" - implementation 'com.android.support.constraint:constraint-layout:1.1.0' + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation "com.android.support:cardview-v7:28.0.0" + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation 'io.reactivex.rxjava2:rxjava:2.1.13' + implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' } diff --git a/sample/src/main/java/io/constructor/sample/MainActivity.kt b/sample/src/main/java/io/constructor/sample/MainActivity.kt index 0a963b54..5eda6e4b 100755 --- a/sample/src/main/java/io/constructor/sample/MainActivity.kt +++ b/sample/src/main/java/io/constructor/sample/MainActivity.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import io.constructor.core.ConstructorIo +import io.reactivex.schedulers.Schedulers import kotlinx.android.synthetic.main.activity_main.* import java.util.* @@ -18,5 +19,8 @@ class MainActivity : AppCompatActivity() { button3.setOnClickListener { ConstructorIo.trackConversion("testId", "id", 11.0) } button4.setOnClickListener { ConstructorIo.trackSearchResultClick("testTerm", "testId", "1") } button5.setOnClickListener { ConstructorIo.trackSearchResultsLoaded("testTerm", Random().nextInt(99) + 1) } + button6.setOnClickListener { ConstructorIo.getSearchResults("corn").subscribeOn(Schedulers.io()).subscribe { + + } } } } diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index ea623bb5..03103d6d 100755 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -55,4 +55,14 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button4" /> + +