From 07ca8f445fe8634ba50d7a5793ea548c851312d8 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Mon, 25 May 2020 12:44:09 -0400 Subject: [PATCH 1/7] Update gitignore and also files --- .../java/io/constructor/core/Constants.kt | 3 +- .../java/io/constructor/core/ConstructorIo.kt | 217 ++++---- .../java/io/constructor/data/DataManager.kt | 10 +- .../data/interceptor/TokenInterceptor.kt | 33 ++ .../data/local/PreferencesHelper.kt | 14 +- .../model/dataadapter/ResultDataAdapter.kt | 56 ++ .../data/model/search/FacetOption.kt | 4 +- .../data/model/search/ResultData.kt | 3 +- .../data/model/search/ResultFacet.kt | 4 +- .../data/model/search/SearchData.kt | 2 +- .../data/model/search/SearchFacet.kt | 3 +- .../data/model/search/SearchGroup.kt | 2 +- .../data/model/search/SearchResult.kt | 6 + .../data/model/search/SortOption.kt | 10 + .../constructor/data/remote/ConstructorApi.kt | 3 +- .../injection/module/NetworkModule.kt | 14 +- .../ui/suggestion/SuggestionsPresenter.kt | 8 +- .../java/io/constructor/util/Extensions.kt | 2 + .../io/constructor/core/ConstructorIoTest.kt | 281 ++++++++++ .../constructor/data/DataManagerHttpTest.kt | 513 ++++++++++++++++++ .../io/constructor/data/DataManagerTest.kt | 231 ++++++++ .../data/local/PreferencesHelperTest.kt | 69 +-- 22 files changed, 1310 insertions(+), 178 deletions(-) create mode 100755 library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt create mode 100644 library/src/main/java/io/constructor/data/model/dataadapter/ResultDataAdapter.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/SearchResult.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/SortOption.kt create mode 100755 library/src/test/java/io/constructor/core/ConstructorIoTest.kt create mode 100755 library/src/test/java/io/constructor/data/DataManagerHttpTest.kt create mode 100755 library/src/test/java/io/constructor/data/DataManagerTest.kt diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 0b7ccc5b..c6a5e86e 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -29,9 +29,10 @@ class Constants { const val TERM_UNKNOWN = "TERM_UNKNOWN" const val PAGE = "page" const val PER_PAGE = "num_results_per_page" + const val SORT_BY = "sort_by" + const val SORT_ORDER = "sort_order" const val FILTER_GROUP_ID = "filters[group_id]" const val FILTER_FACET = "filters[%s]" - const val RESULT_ID = "result_id" } 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 6ddaeeb5..5f9d6c14 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -14,9 +14,11 @@ import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule import io.constructor.injection.module.NetworkModule import io.constructor.util.broadcastIntent +import io.constructor.util.d +import io.constructor.util.e import io.constructor.util.urlEncode -import io.reactivex.Completable import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers import java.util.* @@ -29,7 +31,7 @@ object ConstructorIo { private lateinit var preferenceHelper: PreferencesHelper private lateinit var configMemoryHolder: ConfigMemoryHolder private lateinit var context: Context - private var broadcast = true + private var disposable = CompositeDisposable() var userId: String? get() = configMemoryHolder.userId @@ -44,8 +46,16 @@ object ConstructorIo { .build() } - private var sessionIncrementHandler: (String) -> Unit = { - trackSessionStartInternal() + private var sessionIncrementEventHandler: (String) -> Unit = { + trackSessionStartInternal(it) + } + + private fun trackSessionStartInternal(sessionId: String, errorCallback: ConstructorError = null) { + disposable.add(dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to sessionId, + Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SESSION_START)).subscribeOn(Schedulers.io()).subscribe({}, { + errorCallback?.invoke(it) + d("Error triggering Session Change event") + })) } fun init(context: Context?, constructorIoConfig: ConstructorIoConfig) { @@ -58,7 +68,7 @@ object ConstructorIo { configMemoryHolder = component.configMemoryHolder() configMemoryHolder.autocompleteResultCount = constructorIoConfig.autocompleteResultCount configMemoryHolder.testCellParams = constructorIoConfig.testCells - preferenceHelper.apiKey = constructorIoConfig.apiKey + preferenceHelper.token = constructorIoConfig.apiKey preferenceHelper.defaultItemSection = constructorIoConfig.defaultItemSection if (preferenceHelper.id.isBlank()) { @@ -78,11 +88,14 @@ object ConstructorIo { this.dataManager = dataManager this.preferenceHelper = preferenceHelper this.configMemoryHolder = configMemoryHolder - this.broadcast = false + preferenceHelper.token = constructorIoConfig.apiKey + if (preferenceHelper.id.isBlank()) { + preferenceHelper.id = UUID.randomUUID().toString() + } } fun appMovedToForeground() { - preferenceHelper.getSessionId(sessionIncrementHandler) + preferenceHelper.getSessionId(sessionIncrementEventHandler) } fun getAutocompleteResults(query: String): Observable?>> { @@ -93,13 +106,24 @@ 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> { - preferenceHelper.getSessionId(sessionIncrementHandler) + fun getSearchResults(text: String, facets: List>>? = null, page: Int? = null, perPage: Int? = null, groupId: Int? = null, sortBy: String? = null, sortOrder: String? = 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()) } - facets.forEach { facet -> + 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()) + } + sortBy?.let { + encodedParams.add(Constants.QueryConstants.SORT_BY.urlEncode() to it.urlEncode()) + } + sortOrder?.let { + encodedParams.add(Constants.QueryConstants.SORT_ORDER.urlEncode() to it.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()) } @@ -107,115 +131,106 @@ object ConstructorIo { return dataManager.getSearchResults(text, encodedParams = encodedParams.toTypedArray()) } - /** - * Tracks Session Start Events - */ - internal fun trackSessionStartInternal (): Completable { - return dataManager.trackSessionStart( - arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SESSION_START) - ) - } - - /** - * Tracks input focus events - */ - fun trackInputFocus(term: String?): Completable { - preferenceHelper.getSessionId(sessionIncrementHandler) - return dataManager.trackInputFocus(term, arrayOf( - Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS - )); - } - - /** - * Tracks autocomplete select events - */ - fun trackAutocompleteSelect(searchTerm: String, originalQuery: String, sectionName: String, group: Group? = null, resultID: String? = null): Completable { - preferenceHelper.getSessionId(sessionIncrementHandler) + fun trackAutocompleteSelect(searchTerm: String, originalQuery: String, sectionName: String, group: Group? = null, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - resultID?.let { encodedParams.add(Constants.QueryConstants.RESULT_ID.urlEncode() to it.urlEncode()) } - val completable = dataManager.trackAutocompleteSelect(searchTerm, arrayOf( - Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionName, - Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, - Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_CLICK - ), encodedParams.toTypedArray()).subscribeOn(Schedulers.io()) - - if (this.broadcast) { - completable.subscribeOn(Schedulers.io()).subscribe { - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) - } - } - - return completable + disposable.add(dataManager.trackAutocompleteSelect(searchTerm, + arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionName, + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, + Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_CLICK), + encodedParams.toTypedArray()) + .subscribe({ + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) + }, { t -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Autocomplete Select event error: ${t.message}") + })) } - /** - * Tracks search submit events - */ - fun trackSearchSubmit(searchTerm: String, originalQuery: String, group: Group?): Completable { - preferenceHelper.getSessionId(sessionIncrementHandler) + fun trackSearchSubmit(searchTerm: String, originalQuery: String, group: Group?, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - val completable = dataManager.trackSearchSubmit(searchTerm, arrayOf( - Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, - Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_SEARCH - ), encodedParams.toTypedArray()) - - if (this.broadcast) { - completable.subscribeOn(Schedulers.io()).subscribe { - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) - } - } - - return completable + disposable.add(dataManager.trackSearchSubmit(searchTerm, + arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, + Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_SEARCH), encodedParams.toTypedArray()) + .subscribe({ + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) + }, { + it.printStackTrace() + errorCallback?.invoke(it) + e("Search Submit event error: ${it.message}") + })) } - /** - * Tracks search results loaded (a.k.a. search results viewed) events - */ - fun trackSearchResultsLoaded(term: String, resultCount: Int): Completable { - preferenceHelper.getSessionId(sessionIncrementHandler) - return dataManager.trackSearchResultsLoaded(term, resultCount, arrayOf( - Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS - )) + 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) + 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 -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Conversion event error: ${t.message}") + })) } - /** - * Tracks search result click events - */ - fun trackSearchResultClick(itemName: String, customerId: String, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, resultID: String? = null): Completable { - preferenceHelper.getSessionId(sessionIncrementHandler) - val encodedParams: ArrayList> = arrayListOf() - resultID?.let { encodedParams.add(Constants.QueryConstants.RESULT_ID.urlEncode() to it.urlEncode()) } + fun trackSearchResultClick(itemName: String, customerId: String, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val sName = sectionName ?: preferenceHelper.defaultItemSection - return dataManager.trackSearchResultClick(itemName, customerId, searchTerm, arrayOf( - Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName - ), encodedParams.toTypedArray()) + disposable.add(dataManager.trackSearchResultClick(itemName, customerId, searchTerm, + arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName)).subscribeOn(Schedulers.io()) + .subscribe({}, { t -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Search SearchResult Click event error: ${t.message}") + })) + } + fun trackSearchResultsLoaded(term: String, resultCount: Int, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + disposable.add(dataManager.trackSearchResultsLoaded(term, resultCount, + arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).subscribeOn(Schedulers.io()) + .subscribe({}, { t -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Search Results Loaded event error: ${t.message}") + })) } - /** - * Tracks conversion (a.k.a add to cart) events - */ - fun trackConversion(itemName: String, customerId: String, revenue: Double?, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null): Completable { - preferenceHelper.getSessionId(sessionIncrementHandler) - val revenueString = revenue?.let { "%.2f".format(revenue) } - return dataManager.trackConversion(searchTerm, itemName, customerId, revenueString, arrayOf( - Constants.QueryConstants.AUTOCOMPLETE_SECTION to (sectionName ?: preferenceHelper.defaultItemSection) - )) + fun trackInputFocus(term: String?, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + disposable.add(dataManager.trackInputFocus(term, + arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).subscribeOn(Schedulers.io()) + .subscribe({}, { t -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Input Focus event error: ${t.message}") + })) } - /** - * Tracks purchase events - */ - fun trackPurchase(clientIds: Array, revenue: Double?, orderID: String, sectionName: String? = null): Completable { - preferenceHelper.getSessionId(sessionIncrementHandler) + 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.AUTOCOMPLETE_SECTION to sectionNameParam) - return dataManager.trackPurchase(clientIds.toList(), revenueString, orderID, params.toTypedArray()) + 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}") + })) } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index aa1811e7..6fe02d6a 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -53,7 +53,7 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi }.toObservable() } - fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { return constructorApi.trackAutocompleteSelect(term, params.toMap(), encodedParams.toMap()) } @@ -69,8 +69,8 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi return constructorApi.trackConversion(term, itemName, customerId, revenue, params.toMap()) } - fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { - return constructorApi.trackSearchResultClick(term, itemName, customerId, params.toMap(), encodedParams.toMap()) + fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array> = arrayOf()): Completable { + return constructorApi.trackSearchResultClick(term, itemName, customerId, params.toMap()) } fun trackSearchResultsLoaded(term: String, resultCount: Int, params: Array>): Completable { @@ -81,8 +81,8 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi return constructorApi.trackInputFocus(term, params.toMap()) } - fun trackPurchase(customerIds: List, revenue: String? = null, orderID: String, params: Array>): Completable { - return constructorApi.trackPurchase(customerIds, revenue, orderID, 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/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt new file mode 100755 index 00000000..98e5cc09 --- /dev/null +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -0,0 +1,33 @@ +package io.constructor.data.interceptor + +import android.content.Context +import io.constructor.BuildConfig +import io.constructor.core.Constants +import io.constructor.data.local.PreferencesHelper +import io.constructor.data.memory.ConfigMemoryHolder +import okhttp3.Interceptor +import okhttp3.Response + + +class TokenInterceptor(val context: Context, private val preferencesHelper: PreferencesHelper, private val configMemoryHolder: ConfigMemoryHolder) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + val builder = request.url().newBuilder() + .addQueryParameter(Constants.QueryConstants.API_KEY, preferencesHelper.token) + .addQueryParameter(Constants.QueryConstants.IDENTITY, preferencesHelper.id) + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, System.currentTimeMillis().toString()) + .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) + configMemoryHolder.testCellParams.forEach { + it?.let { + builder.addQueryParameter(it.first, it.second) + } + } + configMemoryHolder.userId?.let { + builder.addQueryParameter(Constants.QueryConstants.USER_ID, it) + } + val url = builder.build() + request = request.newBuilder().url(url).build() + return chain.proceed(request) + } +} \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt index 28600b26..c1ca2f5b 100755 --- a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt +++ b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt @@ -14,17 +14,17 @@ constructor(@ApplicationContext context: Context, prefFileName: String = PREF_FI get() = preferences.getString(PREF_ID, "") set(value) = preferences.edit().putString(PREF_ID, value).apply() - var apiKey: String - get() = preferences.getString(PREF_API_KEY, "") - set(value) = preferences.edit().putString(PREF_API_KEY, value).apply() + var token: String + get() = preferences.getString(PREF_TOKEN, "") + set(value) = preferences.edit().putString(PREF_TOKEN, value).apply() var defaultItemSection: String get() = preferences.getString(PREF_DEFAULT_ITEM_SECTION, "") set(value) = preferences.edit().putString(PREF_DEFAULT_ITEM_SECTION, value).apply() var groupsShownForFirstTerm: Int - get() = preferences.getInt(PREF_GROUPS_SHOWN_FOR_FIRST_TERM, 2) - set(value) = preferences.edit().putInt(PREF_GROUPS_SHOWN_FOR_FIRST_TERM, value).apply() + get() = preferences.getInt(GROUPS_SHOWN_FOR_FIRST_TERM, 2) + set(value) = preferences.edit().putInt(GROUPS_SHOWN_FOR_FIRST_TERM, value).apply() var lastSessionAccess: Long get() = preferences.getLong(SESSION_LAST_ACCESS, System.currentTimeMillis()) @@ -57,9 +57,9 @@ constructor(@ApplicationContext context: Context, prefFileName: String = PREF_FI } companion object { - const val PREF_API_KEY = "api key" + const val PREF_TOKEN = "token" const val PREF_DEFAULT_ITEM_SECTION = "default_item_section" - const val PREF_GROUPS_SHOWN_FOR_FIRST_TERM = "groups_shown_for_first_term" + const val GROUPS_SHOWN_FOR_FIRST_TERM = "groups_shown_for_first_term" const val PREF_ID = "id" const val PREF_FILE_NAME = "constructor_pref_file" const val SESSION_ID = "session_id" diff --git a/library/src/main/java/io/constructor/data/model/dataadapter/ResultDataAdapter.kt b/library/src/main/java/io/constructor/data/model/dataadapter/ResultDataAdapter.kt new file mode 100644 index 00000000..3a40fefe --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/dataadapter/ResultDataAdapter.kt @@ -0,0 +1,56 @@ +package io.constructor.data.model.dataadapter + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import io.constructor.data.model.Group +import io.constructor.data.model.search.ResultData +import io.constructor.data.model.search.ResultFacet + +class ResultDataAdapter { + + companion object { + val NAMES = JsonReader.Options.of("id", "description", "image_url", "url", "facets", "groups") + } + + @FromJson fun fromJson(jsonReader: JsonReader, facetDelegate: JsonAdapter>, groupDelegate: JsonAdapter>): ResultData { + jsonReader.beginObject() + var metadata: HashMap = hashMapOf() + var id = "" + var description: String? = null + var imageUrl: String? = null + var url: String? = null + var facets: List? = null + var groups: List? = null + while (jsonReader.hasNext()) { + when (jsonReader.selectName(NAMES)) { + 0 -> { + id = jsonReader.nextString() + } + 1 -> { + description = jsonReader.nextString() + } + 2 -> { + imageUrl = jsonReader.nextString() + } + 3 -> { + url = jsonReader.nextString() + + } + 4 -> { + facets = facetDelegate.fromJsonValue(jsonReader.readJsonValue()) + } + 5 -> { + groups = groupDelegate.fromJsonValue(jsonReader.readJsonValue()) + } + else -> { + metadata[jsonReader.nextName()] = jsonReader.readJsonValue() + } + } + } + jsonReader.endObject() + return ResultData(description, id, imageUrl, url, facets, groups, metadata) + + } + +} \ 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 index 3900e7c5..756ed5c8 100644 --- a/library/src/main/java/io/constructor/data/model/search/FacetOption.kt +++ b/library/src/main/java/io/constructor/data/model/search/FacetOption.kt @@ -1,3 +1,5 @@ package io.constructor.data.model.search -data class FacetOption(val count: Int, val value: String?) \ No newline at end of file +import java.io.Serializable + +data class FacetOption(val count: Int, val value: String?) : Serializable \ No newline at end of file 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 index 5595199e..f486478e 100644 --- a/library/src/main/java/io/constructor/data/model/search/ResultData.kt +++ b/library/src/main/java/io/constructor/data/model/search/ResultData.kt @@ -2,6 +2,7 @@ package io.constructor.data.model.search import com.squareup.moshi.Json import io.constructor.data.model.Group +import java.io.Serializable data class ResultData(val description: String?, val id: String, @@ -9,4 +10,4 @@ data class ResultData(val description: String?, val url: String?, val facets: List?, val groups: List?, - var metadata: Map?) \ No newline at end of file + var metadata: Map) : Serializable \ 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 index d92e7986..c917049a 100644 --- a/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt +++ b/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt @@ -1,3 +1,5 @@ package io.constructor.data.model.search -data class ResultFacet(val name: String, val values: List?) +import java.io.Serializable + +data class ResultFacet(val name: String, val values: List?) : Serializable 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 index f40a9a98..c7755287 100644 --- a/library/src/main/java/io/constructor/data/model/search/SearchData.kt +++ b/library/src/main/java/io/constructor/data/model/search/SearchData.kt @@ -3,4 +3,4 @@ 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) +data class SearchData(val facets: List?, val groups: List?, @Json(name = "results") val searchResults: List?, @Json(name = "sort_options") val sortOptions: List? = null, @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 index fa861f52..6a4ae44b 100644 --- a/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt +++ b/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt @@ -1,6 +1,7 @@ package io.constructor.data.model.search import com.squareup.moshi.Json +import java.io.Serializable data class SearchFacet(val name: String, @Json(name = "display_name") val displayName: String?, @@ -8,4 +9,4 @@ data class SearchFacet(val name: String, val type: String?, val min: Int?, val max: Int?, - val options: List?) + val options: List?) : Serializable 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 index 8dbe9fa2..d8dfef16 100644 --- a/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt +++ b/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt @@ -4,6 +4,6 @@ import com.squareup.moshi.Json data class SearchGroup(@Json(name = "children") val children: List?, @Json(name = "parents") val parents: List?, - val count: Int, + 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/SearchResult.kt b/library/src/main/java/io/constructor/data/model/search/SearchResult.kt new file mode 100644 index 00000000..8652b381 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchResult.kt @@ -0,0 +1,6 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json +import java.io.Serializable + +data class SearchResult(@Json(name = "data") val result: ResultData, @Json(name = "matched_terms") val matchedTerms: List?, val value: String) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/search/SortOption.kt b/library/src/main/java/io/constructor/data/model/search/SortOption.kt new file mode 100644 index 00000000..1a674ada --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SortOption.kt @@ -0,0 +1,10 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json +import java.io.Serializable + + +data class SortOption(@Json(name = "display_name") val displayName: String, + @Json(name = "sort_by") val sortBy: String, + @Json(name = "sort_order") val sortOrder: String, + val status: String) : Serializable 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 8035f2b2..de40e272 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -26,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 trackSearchResultClick(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @QueryMap params: Map, @QueryMap(encoded = true) encodedData: 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 @@ -37,7 +37,6 @@ interface ConstructorApi { @GET(ApiPaths.URL_PURCHASE) fun trackPurchase(@Query(Constants.QueryConstants.CUSTOMER_ID) customerIds: List, @Query("revenue") revenue: String?, - @Query("order_id") orderID: String, @QueryMap params: Map): Completable @GET diff --git a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt index b1008f12..afddbbea 100755 --- a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt +++ b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt @@ -1,14 +1,15 @@ package io.constructor.injection.module import android.content.Context -import com.squareup.moshi.KotlinJsonAdapterFactory import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.Module import dagger.Provides import io.constructor.BuildConfig -import io.constructor.data.interceptor.RequestInterceptor +import io.constructor.data.interceptor.TokenInterceptor import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.model.dataadapter.ResultDataAdapter import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -33,11 +34,11 @@ class NetworkModule(private val context: Context) { @Provides @Singleton internal fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor, - requestInterceptor: RequestInterceptor): OkHttpClient { + tokenInterceptor: TokenInterceptor): OkHttpClient { val httpClientBuilder = OkHttpClient.Builder() - httpClientBuilder.addInterceptor(requestInterceptor) + httpClientBuilder.addInterceptor(tokenInterceptor) if (BuildConfig.DEBUG) { - // httpClientBuilder.addInterceptor(httpLoggingInterceptor) + httpClientBuilder.addInterceptor(httpLoggingInterceptor) } return httpClientBuilder.build() @@ -50,12 +51,13 @@ class NetworkModule(private val context: Context) { @Provides @Singleton - internal fun provideRequestInterceptor(prefHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder): RequestInterceptor = RequestInterceptor(context, prefHelper, configMemoryHolder) + internal fun provideTokenInterceptor(prefHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder): TokenInterceptor = TokenInterceptor(context, prefHelper, configMemoryHolder) @Provides @Singleton internal fun provideMoshi(): Moshi = Moshi .Builder() + .add(ResultDataAdapter()) .add(KotlinJsonAdapterFactory()) .build() } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt index f7239057..a042b03f 100755 --- a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt +++ b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt @@ -40,8 +40,8 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter @@ -52,8 +52,8 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter() + private val pref = mockk() + private val configMemoryHolder = mockk() + private val data = mockk() + private var constructorIo = ConstructorIo + private val sampleMillis = "1520000000000" + private val dummySuggestion = SuggestionViewModel("", Group("123", "Test name", null), "", null) + + @Before + fun setUp() { + every { ctx.applicationContext } returns ctx + every { pref.token = any() } returns Unit + every { pref.id } returns "1" + every { pref.getSessionId() } returns 1 + every { pref.getSessionId(any()) } returns 1 + every { pref.getSessionId(any(), any()) } returns 1 + constructorIo.testInit(ctx, ConstructorIoConfig("dummyKey", + testCells = listOf("1" to "2", "3" to "4")), data, pref, configMemoryHolder) + } + + @After + fun tearDown() { + } + + @Test + fun verifySelectUrl() { + val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/select?s=1&i=1&_dt=1520000000000&autocomplete_section=Search%20Suggestions&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=click&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" + val searchQuery = "dog" + val term = "hot dogs" + val urlBuilder = HttpUrl.Builder().scheme("https") + .host("ac.cnstrc.com") + .addPathSegment("autocomplete") + .addPathSegment(term) + .addPathSegment("select") + .addQueryParameter(Constants.QueryConstants.SESSION, "1") + .addQueryParameter(Constants.QueryConstants.IDENTITY, "1") + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) + .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_SECTION, Constants.QueryValues.SEARCH_SUGGESTIONS) + .addQueryParameter(Constants.QueryConstants.ORIGINAL_QUERY, searchQuery) + .addEncodedQueryParameter(Constants.QueryConstants.GROUP_ID.urlEncode(), "Meat%20%26%20Seafood") + .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) + .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_CLICK) + .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") + val urlString = urlBuilder.build().url().toString() + assertEquals(expected, urlString) + } + + @Test + fun verifyGetSuggestionsUrl() { + val expected = "https://ac.cnstrc.com/autocomplete/dog?key=testKey&_dt=1520000000000" + val searchQuery = "dog" + val urlBuilder = HttpUrl.Builder().scheme("https") + .host("ac.cnstrc.com") + .addPathSegment("autocomplete") + .addPathSegment(searchQuery) + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) + val urlString = urlBuilder.build().url().toString() + assertEquals(expected, urlString) + } + + @Test + fun verifySessionStartEventUrl() { + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=session_start&key=testKey&_dt=1520000000000" + val urlBuilder = HttpUrl.Builder().scheme("https") + .host("ac.cnstrc.com") + .addPathSegment("behavior") + .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) + .addQueryParameter(Constants.QueryConstants.SESSION, "1") + .addQueryParameter(Constants.QueryConstants.ACTION, "session_start") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) + val urlString = urlBuilder.build().url().toString() + assertEquals(expected, urlString) + } + + @Test + fun verifySearchResultClickEventUrl() { + val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=${BuildConfig.CLIENT_VERSION}&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" + val urlBuilder = HttpUrl.Builder().scheme("https") + .host("ac.cnstrc.com") + .addPathSegment("autocomplete") + .addPathSegment("term") + .addPathSegment("click_through") + .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) + .addQueryParameter(Constants.QueryConstants.SESSION, "1") + .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_SECTION, "Products") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) + val urlString = urlBuilder.build().url().toString() + assertEquals(expected, urlString) + } + + @Test + fun verifySearchResultsLoadedEventUrl() { + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=search-searchResults&key=testKey&_dt=1520000000000" + val urlBuilder = HttpUrl.Builder().scheme("https") + .host("ac.cnstrc.com") + .addPathSegment("behavior") + .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) + .addQueryParameter(Constants.QueryConstants.SESSION, "1") + .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_SEARCH_RESULTS) + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) + val urlString = urlBuilder.build().url().toString() + assertEquals(expected, urlString) + } + + @Test + fun verifyInputFocusEvent() { + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" + val urlBuilder = HttpUrl.Builder().scheme("https") + .host("ac.cnstrc.com") + .addPathSegment("behavior") + .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) + .addQueryParameter(Constants.QueryConstants.IDENTITY, "user_id") + .addQueryParameter(Constants.QueryConstants.SESSION, "1") + .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_INPUT_FOCUS) + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) + val urlString = urlBuilder.build().url().toString() + assertEquals(expected, urlString) + } + + @Test + fun trackAutocompleteSelectSuccess() { + staticMockk("io.constructor.util.ExtensionsKt").use { + every { ctx.broadcastIntent(any(), any()) } returns Unit + every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.complete() + constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) + verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } + } + } + + @Test + fun trackAutocompleteSelectError() { + staticMockk("io.constructor.util.ExtensionsKt").use { + every { ctx.broadcastIntent(any(), any()) } returns Unit + every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.error(Exception()) + constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) + verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } + } + } + + @Test + fun trackSearchSubmitSuccess() { + staticMockk("io.constructor.util.ExtensionsKt").use { + every { ctx.broadcastIntent(any(), any()) } returns Unit + every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() + constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) + verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } + } + } + + @Test + fun verifySearchUrl() { + val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/search?s=1&i=1&_dt=1520000000000&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=search&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" + val originalQuery = "dog" + val term = "hot dogs" + val urlBuilder = HttpUrl.Builder().scheme("https") + .host("ac.cnstrc.com") + .addPathSegment("autocomplete") + .addPathSegment(term) + .addPathSegment("search") + .addQueryParameter(Constants.QueryConstants.SESSION, "1") + .addQueryParameter(Constants.QueryConstants.IDENTITY, "1") + .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) + .addQueryParameter(Constants.QueryConstants.ORIGINAL_QUERY, originalQuery) + .addEncodedQueryParameter(Constants.QueryConstants.GROUP_ID.urlEncode(), "Meat%20%26%20Seafood") + .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) + .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_SEARCH) + .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") + val urlString = urlBuilder.build().url().toString() + assertEquals(expected, urlString) + } + + @Test + fun verifyTestCellParamsAddedToRequest() { + val mockServer = MockWebServer() + every { pref.token } returns "123" + every { pref.id } returns "1" + every { configMemoryHolder.testCellParams = any() } just Runs + every { configMemoryHolder.userId } returns "uid" + every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) + every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") + mockServer.start() + mockServer.enqueue(MockResponse()) + var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).build() + client.newCall(Request.Builder().url(mockServer.url("/")).build()).execute() + var recordedRequest = mockServer.takeRequest() + assert(recordedRequest.path.contains("ef-1=2")) + assert(recordedRequest.path.contains("ui=uid")) + } + + @Test + fun trackSearchSubmitError() { + staticMockk("io.constructor.util.ExtensionsKt").use { + every { ctx.broadcastIntent(any(), any()) } returns Unit + every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) + constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) + verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } + } + } + + @Test + fun trackConversion() { + every { pref.defaultItemSection } returns "Products" + every { data.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() + constructorIo.trackConversion("corn", "id1", 11.99) + verify(exactly = 1) { data.trackConversion("TERM_UNKNOWN", any(), any(), any(), any()) } + } + + @Test + fun trackSearchResultClick() { + every { pref.defaultItemSection } returns "Products" + every { data.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.complete() + constructorIo.trackSearchResultClick("1", "1") + verify(exactly = 1) { data.trackSearchResultClick(any(), any(), any(), any()) } + } + + @Test + fun getSessionId() { + constructorIo.getSessionId() + verify(exactly = 1) { pref.getSessionId() } + } + + @Test + fun getClientId() { + constructorIo.getClientId() + verify(exactly = 2) { pref.id } + } + + @Test + fun trackInputFocus() { + every { data.trackInputFocus(any(), any()) } returns Completable.complete() + constructorIo.trackInputFocus("1") + verify(exactly = 1) { data.trackInputFocus(any(), any()) } + } + + @Test + fun trackPurchase() { + every { pref.defaultItemSection } returns "Products" + 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..b31f961a --- /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.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +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.model.dataadapter.ResultDataAdapter +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(ResultDataAdapter()) + .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.searchResults!!.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.searchResults!!.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 new file mode 100755 index 00000000..ee987c7d --- /dev/null +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -0,0 +1,231 @@ +package io.constructor.data + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import io.constructor.data.model.AutocompleteResult +import io.constructor.data.model.dataadapter.ResultDataAdapter +import io.constructor.data.remote.ConstructorApi +import io.constructor.util.RxSchedulersOverrideRule +import io.constructor.util.TestDataLoader +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.reactivex.Completable +import io.reactivex.Single +import okhttp3.MediaType +import okhttp3.ResponseBody +import org.junit.Rule +import org.junit.Test +import retrofit2.Response +import retrofit2.adapter.rxjava2.Result + +class DataManagerTest { + + @Rule + @JvmField val overrideSchedulersRule = RxSchedulersOverrideRule() + + private var constructorApi = mockk() + + private var moshi = Moshi + .Builder() + .add(ResultDataAdapter()) + .add(KotlinJsonAdapterFactory()) + .build() + + private var dataManager = DataManager(constructorApi, moshi) + + @Test + 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 + } + } + + @Test + 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 + } + } + + @Test + fun getAutocompleteResultsException() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.error(Exception())) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.isError + } + } + + @Test + 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 + } + } + + @Test + 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 { + it.isEmpty + } + } + + @Test + fun trackAutocompleteSelect() { + every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.complete() + dataManager.trackAutocompleteSelect("titanic") + verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} + } + + @Test + fun trackAutocompleteSelectError() { + every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.error(Exception()) + val observer = dataManager.trackAutocompleteSelect("titanic").test() + observer.assertError { + true + } + verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} + } + + @Test + fun trackSearchSubmit() { + every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchSubmit("titanic") + verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} + } + + @Test + fun trackSearchSubmitError() { + every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchSubmit("titanic").test() + observer.assertError { + true + } + verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} + } + + @Test + fun trackSessionStart() { + every { constructorApi.trackSessionStart(any()) } returns Completable.complete() + dataManager.trackSessionStart(arrayOf()) + verify(exactly = 1) { constructorApi.trackSessionStart(any())} + } + + @Test + fun trackSessionStartError() { + every { constructorApi.trackSessionStart(any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSessionStart(arrayOf()).test() + observer.assertError { + true + } + verify(exactly = 1) { constructorApi.trackSessionStart(any())} + } + + @Test + fun trackConversion() { + every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() + dataManager.trackConversion("testTerm", "item1", "id1", "11.99") + verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} + } + + @Test + fun trackConversionError() { + every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackConversion("testTerm", "item1", "id1").test() + observer.assertError { + true + } + verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} + } + + @Test + fun trackSearchResultClick() { + every { constructorApi.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchResultClick("term", "id1", "term1") + verify(exactly = 1) { constructorApi.trackSearchResultClick(any(), any(), any(), any())} + } + + @Test + fun trackSearchResultClickError() { + 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.trackSearchResultClick(any(), any(), any(), any())} + } + + @Test + fun trackSearchResultsLoaded() { + every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchResultsLoaded("term", 10, arrayOf()) + verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} + } + + @Test + fun trackSearchResultsLoadedError() { + every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchResultsLoaded("term", 10, arrayOf()).test() + observer.assertError { true } + verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} + } + + @Test + fun trackInputFocus() { + every { constructorApi.trackInputFocus(any(), any()) } returns Completable.complete() + dataManager.trackInputFocus("term", arrayOf()) + verify(exactly = 1) { constructorApi.trackInputFocus(any(), any()) } + } + + @Test + fun trackInputFocusError() { + every { constructorApi.trackInputFocus(any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackInputFocus("term", arrayOf()).test() + observer.assertError { true } + verify(exactly = 1) { constructorApi.trackInputFocus(any(), any()) } + } + + @Test + fun trackPurchase() { + 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/data/local/PreferencesHelperTest.kt b/library/src/test/java/io/constructor/data/local/PreferencesHelperTest.kt index 0ed29057..b262bbe6 100755 --- a/library/src/test/java/io/constructor/data/local/PreferencesHelperTest.kt +++ b/library/src/test/java/io/constructor/data/local/PreferencesHelperTest.kt @@ -2,6 +2,7 @@ package io.constructor.data.local import io.mockk.every import io.mockk.spyk +import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -12,78 +13,54 @@ import java.util.concurrent.TimeUnit @RunWith(RobolectricTestRunner::class) class PreferencesHelperTest { - private val partyLikeIts1999 = 946684800000 - private val preferencesHelper = spyk(PreferencesHelper(RuntimeEnvironment.application.applicationContext, "test.prefs")) + private var preferencesHelper = spyk(PreferencesHelper(RuntimeEnvironment.application.applicationContext, "test.prefs")) - @Test - fun saveAndRetrieveId() { - preferencesHelper.id = "4a31451c-9a6b-417c-81d6-88d4669cfee4" - assertEquals("4a31451c-9a6b-417c-81d6-88d4669cfee4", preferencesHelper.id) + private var dummyAction: (String) -> Unit = { + assertEquals("2", it) } @Test - fun saveAndRetrieveApiKey() { - preferencesHelper.apiKey = "test-key" - assertEquals("test-key", preferencesHelper.apiKey) + fun saveAndRetrieveToken() { + preferencesHelper.token = "testToken" + assertEquals("testToken", preferencesHelper.token) } @Test fun saveAndRetrieveDefaultItemSection() { - preferencesHelper.defaultItemSection = "Sofas" - assertEquals("Sofas", preferencesHelper.defaultItemSection) - } - - @Test - fun saveAndRetrieveGroupsShownForFirstTerm() { - preferencesHelper.groupsShownForFirstTerm = 99 - assertEquals(99, preferencesHelper.groupsShownForFirstTerm) - } - - @Test - fun saveAndRetrieveLastSessionAccess() { - preferencesHelper.lastSessionAccess = partyLikeIts1999 - assertEquals(partyLikeIts1999, preferencesHelper.lastSessionAccess) - } - - @Test - fun getSessionIdFirstTime() { - assertEquals(1, preferencesHelper.getSessionId()) + preferencesHelper.defaultItemSection = "Products" + assertEquals("Products", preferencesHelper.defaultItemSection) } @Test - fun getSessionIdAfter30Minutes() { - preferencesHelper.resetSession(null) + fun getSessionId() { val currentTime = System.currentTimeMillis() + assertEquals(1, preferencesHelper.getSessionId()) + verify(exactly = 1) { preferencesHelper.resetSession(any()) } + assertEquals(1, preferencesHelper.getSessionId()) every { preferencesHelper.lastSessionAccess } returns currentTime - TimeUnit.MINUTES.toMillis(31) assertEquals(2, preferencesHelper.getSessionId()) assertEquals(3, preferencesHelper.getSessionId()) } @Test - fun getSessionIdWithForceIncrement() { - preferencesHelper.resetSession(null) - preferencesHelper.getSessionId(null, true); - assertEquals(2, preferencesHelper.getSessionId()) - } - - @Test - fun getSessionIdWithIncrementAction() { - preferencesHelper.resetSession(null) + fun verifySessionIdIncrementTriggerAction() { val currentTime = System.currentTimeMillis() - val dummyAction: (String) -> Unit = { - assertEquals("2", it) - } + assertEquals(1, preferencesHelper.getSessionId()) + verify(exactly = 1) { preferencesHelper.resetSession(any()) } every { preferencesHelper.lastSessionAccess} returns currentTime - TimeUnit.MINUTES.toMillis(31) assertEquals(2, preferencesHelper.getSessionId(dummyAction)) } @Test - fun clear() { + fun saveAndRetrieveId() { + preferencesHelper.id = "testId" + assertEquals("testId", preferencesHelper.id) + } + + @Test + fun clearAllValues() { preferencesHelper.clear() assertEquals("", preferencesHelper.id) - assertEquals("", preferencesHelper.apiKey) - assertEquals("", preferencesHelper.defaultItemSection) - assertEquals(2, preferencesHelper.groupsShownForFirstTerm) } } \ No newline at end of file From 41f742b934e1a3e6d703280566c8bd0e573dce9b Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Mon, 25 May 2020 12:54:14 -0400 Subject: [PATCH 2/7] Undo changes --- .../java/io/constructor/data/DataManager.kt | 10 +- .../data/interceptor/TokenInterceptor.kt | 33 -- .../data/local/PreferencesHelper.kt | 14 +- .../constructor/data/remote/ConstructorApi.kt | 3 +- .../java/io/constructor/util/Extensions.kt | 2 - .../io/constructor/core/ConstructorIoTest.kt | 281 ---------- .../constructor/data/DataManagerHttpTest.kt | 513 ------------------ .../io/constructor/data/DataManagerTest.kt | 231 -------- .../data/local/PreferencesHelperTest.kt | 69 ++- 9 files changed, 60 insertions(+), 1096 deletions(-) delete mode 100755 library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt delete mode 100755 library/src/test/java/io/constructor/core/ConstructorIoTest.kt delete mode 100755 library/src/test/java/io/constructor/data/DataManagerHttpTest.kt delete mode 100755 library/src/test/java/io/constructor/data/DataManagerTest.kt diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index 6fe02d6a..aa1811e7 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -53,7 +53,7 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi }.toObservable() } - fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { return constructorApi.trackAutocompleteSelect(term, params.toMap(), encodedParams.toMap()) } @@ -69,8 +69,8 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi return constructorApi.trackConversion(term, itemName, customerId, revenue, params.toMap()) } - fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array> = arrayOf()): Completable { - return constructorApi.trackSearchResultClick(term, itemName, customerId, params.toMap()) + fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + return constructorApi.trackSearchResultClick(term, itemName, customerId, params.toMap(), encodedParams.toMap()) } fun trackSearchResultsLoaded(term: String, resultCount: Int, params: Array>): Completable { @@ -81,8 +81,8 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi return constructorApi.trackInputFocus(term, params.toMap()) } - fun trackPurchase(customerIds: List, revenue: String? = null, params: Array>): Completable { - return constructorApi.trackPurchase(customerIds, revenue, params.toMap()) + fun trackPurchase(customerIds: List, revenue: String? = null, orderID: String, params: Array>): Completable { + return constructorApi.trackPurchase(customerIds, revenue, orderID, params.toMap()) } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt deleted file mode 100755 index 98e5cc09..00000000 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.constructor.data.interceptor - -import android.content.Context -import io.constructor.BuildConfig -import io.constructor.core.Constants -import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.ConfigMemoryHolder -import okhttp3.Interceptor -import okhttp3.Response - - -class TokenInterceptor(val context: Context, private val preferencesHelper: PreferencesHelper, private val configMemoryHolder: ConfigMemoryHolder) : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - var request = chain.request() - val builder = request.url().newBuilder() - .addQueryParameter(Constants.QueryConstants.API_KEY, preferencesHelper.token) - .addQueryParameter(Constants.QueryConstants.IDENTITY, preferencesHelper.id) - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, System.currentTimeMillis().toString()) - .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - configMemoryHolder.testCellParams.forEach { - it?.let { - builder.addQueryParameter(it.first, it.second) - } - } - configMemoryHolder.userId?.let { - builder.addQueryParameter(Constants.QueryConstants.USER_ID, it) - } - val url = builder.build() - request = request.newBuilder().url(url).build() - return chain.proceed(request) - } -} \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt index c1ca2f5b..28600b26 100755 --- a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt +++ b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt @@ -14,17 +14,17 @@ constructor(@ApplicationContext context: Context, prefFileName: String = PREF_FI get() = preferences.getString(PREF_ID, "") set(value) = preferences.edit().putString(PREF_ID, value).apply() - var token: String - get() = preferences.getString(PREF_TOKEN, "") - set(value) = preferences.edit().putString(PREF_TOKEN, value).apply() + var apiKey: String + get() = preferences.getString(PREF_API_KEY, "") + set(value) = preferences.edit().putString(PREF_API_KEY, value).apply() var defaultItemSection: String get() = preferences.getString(PREF_DEFAULT_ITEM_SECTION, "") set(value) = preferences.edit().putString(PREF_DEFAULT_ITEM_SECTION, value).apply() var groupsShownForFirstTerm: Int - get() = preferences.getInt(GROUPS_SHOWN_FOR_FIRST_TERM, 2) - set(value) = preferences.edit().putInt(GROUPS_SHOWN_FOR_FIRST_TERM, value).apply() + get() = preferences.getInt(PREF_GROUPS_SHOWN_FOR_FIRST_TERM, 2) + set(value) = preferences.edit().putInt(PREF_GROUPS_SHOWN_FOR_FIRST_TERM, value).apply() var lastSessionAccess: Long get() = preferences.getLong(SESSION_LAST_ACCESS, System.currentTimeMillis()) @@ -57,9 +57,9 @@ constructor(@ApplicationContext context: Context, prefFileName: String = PREF_FI } companion object { - const val PREF_TOKEN = "token" + const val PREF_API_KEY = "api key" const val PREF_DEFAULT_ITEM_SECTION = "default_item_section" - const val GROUPS_SHOWN_FOR_FIRST_TERM = "groups_shown_for_first_term" + const val PREF_GROUPS_SHOWN_FOR_FIRST_TERM = "groups_shown_for_first_term" const val PREF_ID = "id" const val PREF_FILE_NAME = "constructor_pref_file" const val SESSION_ID = "session_id" 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 de40e272..8035f2b2 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -26,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 trackSearchResultClick(@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, @QueryMap(encoded = true) encodedData: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) fun trackSearchResultsLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map): Completable @@ -37,6 +37,7 @@ interface ConstructorApi { @GET(ApiPaths.URL_PURCHASE) fun trackPurchase(@Query(Constants.QueryConstants.CUSTOMER_ID) customerIds: List, @Query("revenue") revenue: String?, + @Query("order_id") orderID: String, @QueryMap params: Map): Completable @GET diff --git a/library/src/main/java/io/constructor/util/Extensions.kt b/library/src/main/java/io/constructor/util/Extensions.kt index 43f7aa26..41b8cae2 100755 --- a/library/src/main/java/io/constructor/util/Extensions.kt +++ b/library/src/main/java/io/constructor/util/Extensions.kt @@ -37,8 +37,6 @@ fun String.urlEncode() = URLEncoder.encode(this, "UTF-8").replace("+", "%20") fun Any.d(msg: String) = Log.d(this::class.qualifiedName, msg) -fun Any.e(msg: String) = Log.e(this::class.qualifiedName, msg) - fun String.base64Encode(): String? { return String(Base64.encode(toByteArray(), Base64.NO_WRAP or Base64.NO_PADDING)) } diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt deleted file mode 100755 index 500f5912..00000000 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ /dev/null @@ -1,281 +0,0 @@ -package io.constructor.core - -import android.content.Context -import io.constructor.BuildConfig -import io.constructor.data.DataManager -import io.constructor.data.interceptor.TokenInterceptor -import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.ConfigMemoryHolder -import io.constructor.data.model.Group -import io.constructor.data.model.SuggestionViewModel -import io.constructor.util.RxSchedulersOverrideRule -import io.constructor.util.broadcastIntent -import io.constructor.util.urlEncode -import io.mockk.* -import io.reactivex.Completable -import okhttp3.HttpUrl -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals - -class ConstructorIoTest { - - @Rule - @JvmField val overrideSchedulersRule = RxSchedulersOverrideRule() - - private val ctx = mockk() - private val pref = mockk() - private val configMemoryHolder = mockk() - private val data = mockk() - private var constructorIo = ConstructorIo - private val sampleMillis = "1520000000000" - private val dummySuggestion = SuggestionViewModel("", Group("123", "Test name", null), "", null) - - @Before - fun setUp() { - every { ctx.applicationContext } returns ctx - every { pref.token = any() } returns Unit - every { pref.id } returns "1" - every { pref.getSessionId() } returns 1 - every { pref.getSessionId(any()) } returns 1 - every { pref.getSessionId(any(), any()) } returns 1 - constructorIo.testInit(ctx, ConstructorIoConfig("dummyKey", - testCells = listOf("1" to "2", "3" to "4")), data, pref, configMemoryHolder) - } - - @After - fun tearDown() { - } - - @Test - fun verifySelectUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/select?s=1&i=1&_dt=1520000000000&autocomplete_section=Search%20Suggestions&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=click&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" - val searchQuery = "dog" - val term = "hot dogs" - val urlBuilder = HttpUrl.Builder().scheme("https") - .host("ac.cnstrc.com") - .addPathSegment("autocomplete") - .addPathSegment(term) - .addPathSegment("select") - .addQueryParameter(Constants.QueryConstants.SESSION, "1") - .addQueryParameter(Constants.QueryConstants.IDENTITY, "1") - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_SECTION, Constants.QueryValues.SEARCH_SUGGESTIONS) - .addQueryParameter(Constants.QueryConstants.ORIGINAL_QUERY, searchQuery) - .addEncodedQueryParameter(Constants.QueryConstants.GROUP_ID.urlEncode(), "Meat%20%26%20Seafood") - .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) - .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_CLICK) - .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") - val urlString = urlBuilder.build().url().toString() - assertEquals(expected, urlString) - } - - @Test - fun verifyGetSuggestionsUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/dog?key=testKey&_dt=1520000000000" - val searchQuery = "dog" - val urlBuilder = HttpUrl.Builder().scheme("https") - .host("ac.cnstrc.com") - .addPathSegment("autocomplete") - .addPathSegment(searchQuery) - .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) - val urlString = urlBuilder.build().url().toString() - assertEquals(expected, urlString) - } - - @Test - fun verifySessionStartEventUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=session_start&key=testKey&_dt=1520000000000" - val urlBuilder = HttpUrl.Builder().scheme("https") - .host("ac.cnstrc.com") - .addPathSegment("behavior") - .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.SESSION, "1") - .addQueryParameter(Constants.QueryConstants.ACTION, "session_start") - .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) - val urlString = urlBuilder.build().url().toString() - assertEquals(expected, urlString) - } - - @Test - fun verifySearchResultClickEventUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=${BuildConfig.CLIENT_VERSION}&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" - val urlBuilder = HttpUrl.Builder().scheme("https") - .host("ac.cnstrc.com") - .addPathSegment("autocomplete") - .addPathSegment("term") - .addPathSegment("click_through") - .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.SESSION, "1") - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_SECTION, "Products") - .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) - val urlString = urlBuilder.build().url().toString() - assertEquals(expected, urlString) - } - - @Test - fun verifySearchResultsLoadedEventUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=search-searchResults&key=testKey&_dt=1520000000000" - val urlBuilder = HttpUrl.Builder().scheme("https") - .host("ac.cnstrc.com") - .addPathSegment("behavior") - .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.SESSION, "1") - .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_SEARCH_RESULTS) - .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) - val urlString = urlBuilder.build().url().toString() - assertEquals(expected, urlString) - } - - @Test - fun verifyInputFocusEvent() { - val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" - val urlBuilder = HttpUrl.Builder().scheme("https") - .host("ac.cnstrc.com") - .addPathSegment("behavior") - .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.IDENTITY, "user_id") - .addQueryParameter(Constants.QueryConstants.SESSION, "1") - .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_INPUT_FOCUS) - .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) - val urlString = urlBuilder.build().url().toString() - assertEquals(expected, urlString) - } - - @Test - fun trackAutocompleteSelectSuccess() { - staticMockk("io.constructor.util.ExtensionsKt").use { - every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.complete() - constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) - verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } - } - } - - @Test - fun trackAutocompleteSelectError() { - staticMockk("io.constructor.util.ExtensionsKt").use { - every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.error(Exception()) - constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) - verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } - } - } - - @Test - fun trackSearchSubmitSuccess() { - staticMockk("io.constructor.util.ExtensionsKt").use { - every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() - constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) - verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } - } - } - - @Test - fun verifySearchUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/search?s=1&i=1&_dt=1520000000000&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=search&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" - val originalQuery = "dog" - val term = "hot dogs" - val urlBuilder = HttpUrl.Builder().scheme("https") - .host("ac.cnstrc.com") - .addPathSegment("autocomplete") - .addPathSegment(term) - .addPathSegment("search") - .addQueryParameter(Constants.QueryConstants.SESSION, "1") - .addQueryParameter(Constants.QueryConstants.IDENTITY, "1") - .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) - .addQueryParameter(Constants.QueryConstants.ORIGINAL_QUERY, originalQuery) - .addEncodedQueryParameter(Constants.QueryConstants.GROUP_ID.urlEncode(), "Meat%20%26%20Seafood") - .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) - .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_SEARCH) - .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") - val urlString = urlBuilder.build().url().toString() - assertEquals(expected, urlString) - } - - @Test - fun verifyTestCellParamsAddedToRequest() { - val mockServer = MockWebServer() - every { pref.token } returns "123" - every { pref.id } returns "1" - every { configMemoryHolder.testCellParams = any() } just Runs - every { configMemoryHolder.userId } returns "uid" - every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) - every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") - mockServer.start() - mockServer.enqueue(MockResponse()) - var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).build() - client.newCall(Request.Builder().url(mockServer.url("/")).build()).execute() - var recordedRequest = mockServer.takeRequest() - assert(recordedRequest.path.contains("ef-1=2")) - assert(recordedRequest.path.contains("ui=uid")) - } - - @Test - fun trackSearchSubmitError() { - staticMockk("io.constructor.util.ExtensionsKt").use { - every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) - constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) - verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } - } - } - - @Test - fun trackConversion() { - every { pref.defaultItemSection } returns "Products" - every { data.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() - constructorIo.trackConversion("corn", "id1", 11.99) - verify(exactly = 1) { data.trackConversion("TERM_UNKNOWN", any(), any(), any(), any()) } - } - - @Test - fun trackSearchResultClick() { - every { pref.defaultItemSection } returns "Products" - every { data.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.complete() - constructorIo.trackSearchResultClick("1", "1") - verify(exactly = 1) { data.trackSearchResultClick(any(), any(), any(), any()) } - } - - @Test - fun getSessionId() { - constructorIo.getSessionId() - verify(exactly = 1) { pref.getSessionId() } - } - - @Test - fun getClientId() { - constructorIo.getClientId() - verify(exactly = 2) { pref.id } - } - - @Test - fun trackInputFocus() { - every { data.trackInputFocus(any(), any()) } returns Completable.complete() - constructorIo.trackInputFocus("1") - verify(exactly = 1) { data.trackInputFocus(any(), any()) } - } - - @Test - fun trackPurchase() { - every { pref.defaultItemSection } returns "Products" - 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 deleted file mode 100755 index b31f961a..00000000 --- a/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt +++ /dev/null @@ -1,513 +0,0 @@ -package io.constructor.data - -import android.content.Context -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -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.model.dataadapter.ResultDataAdapter -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(ResultDataAdapter()) - .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.searchResults!!.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.searchResults!!.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 deleted file mode 100755 index ee987c7d..00000000 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -package io.constructor.data - -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import io.constructor.data.model.AutocompleteResult -import io.constructor.data.model.dataadapter.ResultDataAdapter -import io.constructor.data.remote.ConstructorApi -import io.constructor.util.RxSchedulersOverrideRule -import io.constructor.util.TestDataLoader -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import io.reactivex.Completable -import io.reactivex.Single -import okhttp3.MediaType -import okhttp3.ResponseBody -import org.junit.Rule -import org.junit.Test -import retrofit2.Response -import retrofit2.adapter.rxjava2.Result - -class DataManagerTest { - - @Rule - @JvmField val overrideSchedulersRule = RxSchedulersOverrideRule() - - private var constructorApi = mockk() - - private var moshi = Moshi - .Builder() - .add(ResultDataAdapter()) - .add(KotlinJsonAdapterFactory()) - .build() - - private var dataManager = DataManager(constructorApi, moshi) - - @Test - 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 - } - } - - @Test - 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 - } - } - - @Test - fun getAutocompleteResultsException() { - every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.error(Exception())) - val observer = dataManager.getAutocompleteResults("titanic").test() - observer.assertComplete().assertValue { - it.isError - } - } - - @Test - 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 - } - } - - @Test - 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 { - it.isEmpty - } - } - - @Test - fun trackAutocompleteSelect() { - every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.complete() - dataManager.trackAutocompleteSelect("titanic") - verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} - } - - @Test - fun trackAutocompleteSelectError() { - every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.error(Exception()) - val observer = dataManager.trackAutocompleteSelect("titanic").test() - observer.assertError { - true - } - verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} - } - - @Test - fun trackSearchSubmit() { - every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() - dataManager.trackSearchSubmit("titanic") - verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} - } - - @Test - fun trackSearchSubmitError() { - every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearchSubmit("titanic").test() - observer.assertError { - true - } - verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} - } - - @Test - fun trackSessionStart() { - every { constructorApi.trackSessionStart(any()) } returns Completable.complete() - dataManager.trackSessionStart(arrayOf()) - verify(exactly = 1) { constructorApi.trackSessionStart(any())} - } - - @Test - fun trackSessionStartError() { - every { constructorApi.trackSessionStart(any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSessionStart(arrayOf()).test() - observer.assertError { - true - } - verify(exactly = 1) { constructorApi.trackSessionStart(any())} - } - - @Test - fun trackConversion() { - every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() - dataManager.trackConversion("testTerm", "item1", "id1", "11.99") - verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} - } - - @Test - fun trackConversionError() { - every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackConversion("testTerm", "item1", "id1").test() - observer.assertError { - true - } - verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} - } - - @Test - fun trackSearchResultClick() { - every { constructorApi.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.complete() - dataManager.trackSearchResultClick("term", "id1", "term1") - verify(exactly = 1) { constructorApi.trackSearchResultClick(any(), any(), any(), any())} - } - - @Test - fun trackSearchResultClickError() { - 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.trackSearchResultClick(any(), any(), any(), any())} - } - - @Test - fun trackSearchResultsLoaded() { - every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.complete() - dataManager.trackSearchResultsLoaded("term", 10, arrayOf()) - verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} - } - - @Test - fun trackSearchResultsLoadedError() { - every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearchResultsLoaded("term", 10, arrayOf()).test() - observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} - } - - @Test - fun trackInputFocus() { - every { constructorApi.trackInputFocus(any(), any()) } returns Completable.complete() - dataManager.trackInputFocus("term", arrayOf()) - verify(exactly = 1) { constructorApi.trackInputFocus(any(), any()) } - } - - @Test - fun trackInputFocusError() { - every { constructorApi.trackInputFocus(any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackInputFocus("term", arrayOf()).test() - observer.assertError { true } - verify(exactly = 1) { constructorApi.trackInputFocus(any(), any()) } - } - - @Test - fun trackPurchase() { - 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/data/local/PreferencesHelperTest.kt b/library/src/test/java/io/constructor/data/local/PreferencesHelperTest.kt index b262bbe6..0ed29057 100755 --- a/library/src/test/java/io/constructor/data/local/PreferencesHelperTest.kt +++ b/library/src/test/java/io/constructor/data/local/PreferencesHelperTest.kt @@ -2,7 +2,6 @@ package io.constructor.data.local import io.mockk.every import io.mockk.spyk -import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -13,54 +12,78 @@ import java.util.concurrent.TimeUnit @RunWith(RobolectricTestRunner::class) class PreferencesHelperTest { - private var preferencesHelper = spyk(PreferencesHelper(RuntimeEnvironment.application.applicationContext, "test.prefs")) + private val partyLikeIts1999 = 946684800000 + private val preferencesHelper = spyk(PreferencesHelper(RuntimeEnvironment.application.applicationContext, "test.prefs")) - private var dummyAction: (String) -> Unit = { - assertEquals("2", it) + @Test + fun saveAndRetrieveId() { + preferencesHelper.id = "4a31451c-9a6b-417c-81d6-88d4669cfee4" + assertEquals("4a31451c-9a6b-417c-81d6-88d4669cfee4", preferencesHelper.id) } @Test - fun saveAndRetrieveToken() { - preferencesHelper.token = "testToken" - assertEquals("testToken", preferencesHelper.token) + fun saveAndRetrieveApiKey() { + preferencesHelper.apiKey = "test-key" + assertEquals("test-key", preferencesHelper.apiKey) } @Test fun saveAndRetrieveDefaultItemSection() { - preferencesHelper.defaultItemSection = "Products" - assertEquals("Products", preferencesHelper.defaultItemSection) + preferencesHelper.defaultItemSection = "Sofas" + assertEquals("Sofas", preferencesHelper.defaultItemSection) } @Test - fun getSessionId() { - val currentTime = System.currentTimeMillis() - assertEquals(1, preferencesHelper.getSessionId()) - verify(exactly = 1) { preferencesHelper.resetSession(any()) } + fun saveAndRetrieveGroupsShownForFirstTerm() { + preferencesHelper.groupsShownForFirstTerm = 99 + assertEquals(99, preferencesHelper.groupsShownForFirstTerm) + } + + @Test + fun saveAndRetrieveLastSessionAccess() { + preferencesHelper.lastSessionAccess = partyLikeIts1999 + assertEquals(partyLikeIts1999, preferencesHelper.lastSessionAccess) + } + + @Test + fun getSessionIdFirstTime() { assertEquals(1, preferencesHelper.getSessionId()) + } + + @Test + fun getSessionIdAfter30Minutes() { + preferencesHelper.resetSession(null) + val currentTime = System.currentTimeMillis() every { preferencesHelper.lastSessionAccess } returns currentTime - TimeUnit.MINUTES.toMillis(31) assertEquals(2, preferencesHelper.getSessionId()) assertEquals(3, preferencesHelper.getSessionId()) } @Test - fun verifySessionIdIncrementTriggerAction() { - val currentTime = System.currentTimeMillis() - assertEquals(1, preferencesHelper.getSessionId()) - verify(exactly = 1) { preferencesHelper.resetSession(any()) } - every { preferencesHelper.lastSessionAccess} returns currentTime - TimeUnit.MINUTES.toMillis(31) - assertEquals(2, preferencesHelper.getSessionId(dummyAction)) + fun getSessionIdWithForceIncrement() { + preferencesHelper.resetSession(null) + preferencesHelper.getSessionId(null, true); + assertEquals(2, preferencesHelper.getSessionId()) } @Test - fun saveAndRetrieveId() { - preferencesHelper.id = "testId" - assertEquals("testId", preferencesHelper.id) + fun getSessionIdWithIncrementAction() { + preferencesHelper.resetSession(null) + val currentTime = System.currentTimeMillis() + val dummyAction: (String) -> Unit = { + assertEquals("2", it) + } + every { preferencesHelper.lastSessionAccess} returns currentTime - TimeUnit.MINUTES.toMillis(31) + assertEquals(2, preferencesHelper.getSessionId(dummyAction)) } @Test - fun clearAllValues() { + fun clear() { preferencesHelper.clear() assertEquals("", preferencesHelper.id) + assertEquals("", preferencesHelper.apiKey) + assertEquals("", preferencesHelper.defaultItemSection) + assertEquals(2, preferencesHelper.groupsShownForFirstTerm) } } \ No newline at end of file From fb22033eca853d522b5ea2a04cb9c5465ceca3ea Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Mon, 25 May 2020 12:56:05 -0400 Subject: [PATCH 3/7] More fixes --- .../constructor/injection/module/NetworkModule.kt | 14 ++++++-------- .../ui/suggestion/SuggestionsPresenter.kt | 8 ++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt index afddbbea..b1008f12 100755 --- a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt +++ b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt @@ -1,15 +1,14 @@ package io.constructor.injection.module import android.content.Context +import com.squareup.moshi.KotlinJsonAdapterFactory import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.Module import dagger.Provides import io.constructor.BuildConfig -import io.constructor.data.interceptor.TokenInterceptor +import io.constructor.data.interceptor.RequestInterceptor import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder -import io.constructor.data.model.dataadapter.ResultDataAdapter import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -34,11 +33,11 @@ class NetworkModule(private val context: Context) { @Provides @Singleton internal fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor, - tokenInterceptor: TokenInterceptor): OkHttpClient { + requestInterceptor: RequestInterceptor): OkHttpClient { val httpClientBuilder = OkHttpClient.Builder() - httpClientBuilder.addInterceptor(tokenInterceptor) + httpClientBuilder.addInterceptor(requestInterceptor) if (BuildConfig.DEBUG) { - httpClientBuilder.addInterceptor(httpLoggingInterceptor) + // httpClientBuilder.addInterceptor(httpLoggingInterceptor) } return httpClientBuilder.build() @@ -51,13 +50,12 @@ class NetworkModule(private val context: Context) { @Provides @Singleton - internal fun provideTokenInterceptor(prefHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder): TokenInterceptor = TokenInterceptor(context, prefHelper, configMemoryHolder) + internal fun provideRequestInterceptor(prefHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder): RequestInterceptor = RequestInterceptor(context, prefHelper, configMemoryHolder) @Provides @Singleton internal fun provideMoshi(): Moshi = Moshi .Builder() - .add(ResultDataAdapter()) .add(KotlinJsonAdapterFactory()) .build() } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt index a042b03f..f7239057 100755 --- a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt +++ b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt @@ -40,8 +40,8 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter @@ -52,8 +52,8 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter Date: Mon, 25 May 2020 13:05:53 -0400 Subject: [PATCH 4/7] Fixes --- .../java/io/constructor/core/Constants.kt | 1 + .../java/io/constructor/core/ConstructorIo.kt | 220 +++++++++--------- .../constructor/data/remote/ConstructorApi.kt | 27 ++- .../core/ConstructorioSearchTest.kt | 4 +- 4 files changed, 130 insertions(+), 122 deletions(-) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index c6a5e86e..d2229fe9 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -33,6 +33,7 @@ class Constants { const val SORT_ORDER = "sort_order" const val FILTER_GROUP_ID = "filters[group_id]" const val FILTER_FACET = "filters[%s]" + const val RESULT_ID = "result_id" } 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 5f9d6c14..b2a0e689 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -14,11 +14,9 @@ import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule import io.constructor.injection.module.NetworkModule import io.constructor.util.broadcastIntent -import io.constructor.util.d -import io.constructor.util.e import io.constructor.util.urlEncode +import io.reactivex.Completable import io.reactivex.Observable -import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers import java.util.* @@ -31,7 +29,7 @@ object ConstructorIo { private lateinit var preferenceHelper: PreferencesHelper private lateinit var configMemoryHolder: ConfigMemoryHolder private lateinit var context: Context - private var disposable = CompositeDisposable() + private var broadcast = true var userId: String? get() = configMemoryHolder.userId @@ -46,16 +44,8 @@ object ConstructorIo { .build() } - private var sessionIncrementEventHandler: (String) -> Unit = { - trackSessionStartInternal(it) - } - - private fun trackSessionStartInternal(sessionId: String, errorCallback: ConstructorError = null) { - disposable.add(dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to sessionId, - Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SESSION_START)).subscribeOn(Schedulers.io()).subscribe({}, { - errorCallback?.invoke(it) - d("Error triggering Session Change event") - })) + private var sessionIncrementHandler: (String) -> Unit = { + trackSessionStartInternal() } fun init(context: Context?, constructorIoConfig: ConstructorIoConfig) { @@ -68,7 +58,7 @@ object ConstructorIo { configMemoryHolder = component.configMemoryHolder() configMemoryHolder.autocompleteResultCount = constructorIoConfig.autocompleteResultCount configMemoryHolder.testCellParams = constructorIoConfig.testCells - preferenceHelper.token = constructorIoConfig.apiKey + preferenceHelper.apiKey = constructorIoConfig.apiKey preferenceHelper.defaultItemSection = constructorIoConfig.defaultItemSection if (preferenceHelper.id.isBlank()) { @@ -88,16 +78,16 @@ object ConstructorIo { this.dataManager = dataManager this.preferenceHelper = preferenceHelper this.configMemoryHolder = configMemoryHolder - preferenceHelper.token = constructorIoConfig.apiKey - if (preferenceHelper.id.isBlank()) { - preferenceHelper.id = UUID.randomUUID().toString() - } + this.broadcast = false } fun appMovedToForeground() { - preferenceHelper.getSessionId(sessionIncrementEventHandler) + preferenceHelper.getSessionId(sessionIncrementHandler) } + /** + * Returns a list of autocomplete suggestions + */ fun getAutocompleteResults(query: String): Observable?>> { val params = mutableListOf>() configMemoryHolder.autocompleteResultCount?.entries?.forEach { @@ -106,23 +96,16 @@ object ConstructorIo { return dataManager.getAutocompleteResults(query, params.toTypedArray()) } + /** + * Returns search results including filters, categories, sort options, etc. + */ fun getSearchResults(text: String, facets: List>>? = null, page: Int? = null, perPage: Int? = null, groupId: Int? = null, sortBy: String? = null, sortOrder: String? = 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()) - } - sortBy?.let { - encodedParams.add(Constants.QueryConstants.SORT_BY.urlEncode() to it.urlEncode()) - } - sortOrder?.let { - encodedParams.add(Constants.QueryConstants.SORT_ORDER.urlEncode() to it.urlEncode()) - } - encodedParams.add(Constants.QueryConstants.SESSION.urlEncode() to sessionId.toString().urlEncode()) + 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()) } + sortBy?.let { encodedParams.add(Constants.QueryConstants.SORT_BY.urlEncode() to it.urlEncode()) } + sortOrder?.let { encodedParams.add(Constants.QueryConstants.SORT_ORDER.urlEncode() to it.urlEncode()) } facets?.forEach { facet -> facet.second.forEach { encodedParams.add(Constants.QueryConstants.FILTER_FACET.format(facet.first).urlEncode() to it.urlEncode()) @@ -131,106 +114,115 @@ object ConstructorIo { 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) + /** + * Tracks Session Start Events + */ + internal fun trackSessionStartInternal (): Completable { + return dataManager.trackSessionStart( + arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SESSION_START) + ) + } + + /** + * Tracks input focus events + */ + fun trackInputFocus(term: String?): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + return dataManager.trackInputFocus(term, arrayOf( + Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS + )); + } + + /** + * Tracks autocomplete select events + */ + fun trackAutocompleteSelect(searchTerm: String, originalQuery: String, sectionName: String, group: Group? = null, resultID: String? = null): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) val encodedParams: ArrayList> = arrayListOf() group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - disposable.add(dataManager.trackAutocompleteSelect(searchTerm, - arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionName, - Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, - Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_CLICK), - encodedParams.toTypedArray()) - .subscribe({ - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) - }, { t -> - t.printStackTrace() - errorCallback?.invoke(t) - e("Autocomplete Select event error: ${t.message}") - })) + resultID?.let { encodedParams.add(Constants.QueryConstants.RESULT_ID.urlEncode() to it.urlEncode()) } + val completable = dataManager.trackAutocompleteSelect(searchTerm, arrayOf( + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionName, + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, + Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_CLICK + ), encodedParams.toTypedArray()).subscribeOn(Schedulers.io()) + + if (this.broadcast) { + completable.subscribeOn(Schedulers.io()).subscribe { + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) + } + } + + return completable } - fun trackSearchSubmit(searchTerm: String, originalQuery: String, group: Group?, errorCallback: ConstructorError = null) { - val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + /** + * Tracks search submit events + */ + fun trackSearchSubmit(searchTerm: String, originalQuery: String, group: Group?): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) val encodedParams: ArrayList> = arrayListOf() group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - disposable.add(dataManager.trackSearchSubmit(searchTerm, - arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, - Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_SEARCH), encodedParams.toTypedArray()) - .subscribe({ - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) - }, { - it.printStackTrace() - errorCallback?.invoke(it) - e("Search Submit event error: ${it.message}") - })) + val completable = dataManager.trackSearchSubmit(searchTerm, arrayOf( + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, + Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_SEARCH + ), encodedParams.toTypedArray()) + + if (this.broadcast) { + completable.subscribeOn(Schedulers.io()).subscribe { + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) + } + } + + return completable } - 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) - 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 -> - t.printStackTrace() - errorCallback?.invoke(t) - e("Conversion event error: ${t.message}") - })) + /** + * Tracks search results loaded (a.k.a. search results viewed) events + */ + fun trackSearchResultsLoaded(term: String, resultCount: Int): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + return dataManager.trackSearchResultsLoaded(term, resultCount, arrayOf( + Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS + )) } - fun trackSearchResultClick(itemName: String, customerId: String, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { - val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + /** + * Tracks search result click events + */ + fun trackSearchResultClick(itemName: String, customerId: String, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, resultID: String? = null): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val encodedParams: ArrayList> = arrayListOf() + resultID?.let { encodedParams.add(Constants.QueryConstants.RESULT_ID.urlEncode() to it.urlEncode()) } val sName = sectionName ?: preferenceHelper.defaultItemSection - disposable.add(dataManager.trackSearchResultClick(itemName, customerId, searchTerm, - arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName)).subscribeOn(Schedulers.io()) - .subscribe({}, { t -> - t.printStackTrace() - errorCallback?.invoke(t) - e("Search SearchResult Click event error: ${t.message}") - })) - } + return dataManager.trackSearchResultClick(itemName, customerId, searchTerm, arrayOf( + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName + ), encodedParams.toTypedArray()) - fun trackSearchResultsLoaded(term: String, resultCount: Int, errorCallback: ConstructorError = null) { - val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackSearchResultsLoaded(term, resultCount, - arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).subscribeOn(Schedulers.io()) - .subscribe({}, { t -> - t.printStackTrace() - errorCallback?.invoke(t) - e("Search Results Loaded event error: ${t.message}") - })) } - fun trackInputFocus(term: String?, errorCallback: ConstructorError = null) { - val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackInputFocus(term, - arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).subscribeOn(Schedulers.io()) - .subscribe({}, { t -> - t.printStackTrace() - errorCallback?.invoke(t) - e("Input Focus event error: ${t.message}") - })) + /** + * Tracks conversion (a.k.a add to cart) events + */ + fun trackConversion(itemName: String, customerId: String, revenue: Double?, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val revenueString = revenue?.let { "%.2f".format(revenue) } + return dataManager.trackConversion(searchTerm, itemName, customerId, revenueString, arrayOf( + Constants.QueryConstants.AUTOCOMPLETE_SECTION to (sectionName ?: preferenceHelper.defaultItemSection) + )) } - fun trackPurchase(clientIds: Array, revenue: Double?, sectionName: String? = null, errorCallback: ConstructorError = null) { - val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + /** + * Tracks purchase events + */ + fun trackPurchase(clientIds: Array, revenue: Double?, orderID: String, sectionName: String? = null): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) 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}") - })) + val params = mutableListOf(Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionNameParam) + return dataManager.trackPurchase(clientIds.toList(), revenueString, orderID, params.toTypedArray()) } } \ 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 8035f2b2..4e0203bf 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -11,25 +11,40 @@ import retrofit2.http.* interface ConstructorApi { @GET(ApiPaths.URL_AUTOCOMPLETE) - fun getAutocompleteResults(@Path("value") value: String, @QueryMap data: Map): Single> + 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 + fun trackAutocompleteSelect(@Path("term") term: String, + @QueryMap data: Map, + @QueryMap(encoded = true) encodedData: Map): Completable @GET(ApiPaths.URL_SEARCH_SUBMIT_EVENT) - fun trackSearchSubmit(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable + fun trackSearchSubmit(@Path("term") term: String, + @QueryMap data: Map, + @QueryMap(encoded = true) encodedData: Map): Completable @GET(ApiPaths.URL_SESSION_START_EVENT) fun trackSessionStart(@QueryMap params: Map): Completable @GET(ApiPaths.URL_CONVERSION_EVENT) - fun trackConversion(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @Query("revenue") revenue: String?, @QueryMap params: Map): Completable + 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 trackSearchResultClick(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @QueryMap params: Map, @QueryMap(encoded = true) encodedData: Map): Completable + fun trackSearchResultClick(@Path("term") term: String, + @Query("name") itemName: String, + @Query("customer_id") customerId: String, + @QueryMap params: Map, + @QueryMap(encoded = true) encodedData: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) - fun trackSearchResultsLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map): Completable + fun trackSearchResultsLoaded(@Query("term") term: String, + @Query("num_results") resultCount: Int, + @QueryMap params: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map): Completable diff --git a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt index 819852bb..1211224e 100644 --- a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt @@ -54,7 +54,7 @@ class ConstructorIoSearchTest { mockServer.enqueue(mockResponse) val observer = constructorIo.getSearchResults("corn").test() observer.assertComplete().assertValue { - it.get()!!.searchData.results!!.size == 20 + it.get()!!.searchData.searchResults!!.size == 20 } val request = mockServer.takeRequest() val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt=" @@ -107,7 +107,7 @@ class ConstructorIoSearchTest { mockServer.enqueue(mockResponse) val observer = constructorIo.getSearchResults("corn").test() observer.assertComplete().assertValue { - it.get()!!.searchData.results!!.isEmpty() + it.get()!!.searchData.searchResults!!.isEmpty() } val request = mockServer.takeRequest() val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt=" From 1001e362827a8cd4b412c1aa4ec9d71b2844c521 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Mon, 25 May 2020 13:28:51 -0400 Subject: [PATCH 5/7] Tweaks --- .../data/model/search/SearchFacet.kt | 4 +- .../data/model/search/SearchGroup.kt | 2 +- .../injection/module/NetworkModule.kt | 2 + .../core/ConstructorioSearchTest.kt | 2 +- .../src/test/resources/search_response.json | 1887 ++++++++++++----- .../test/resources/search_response_empty.json | 23 +- 6 files changed, 1359 insertions(+), 561 deletions(-) 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 index 6a4ae44b..f934dba8 100644 --- a/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt +++ b/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt @@ -7,6 +7,6 @@ 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 min: Double?, + val max: Double?, val options: List?) : Serializable 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 index d8dfef16..a580ef70 100644 --- a/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt +++ b/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt @@ -6,4 +6,4 @@ 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 + @Json(name = "group_id") val groupId: String) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt index b1008f12..e205e9c2 100755 --- a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt +++ b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt @@ -9,6 +9,7 @@ import io.constructor.BuildConfig import io.constructor.data.interceptor.RequestInterceptor import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.model.dataadapter.ResultDataAdapter import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -56,6 +57,7 @@ class NetworkModule(private val context: Context) { @Singleton internal fun provideMoshi(): Moshi = Moshi .Builder() + .add(ResultDataAdapter()) .add(KotlinJsonAdapterFactory()) .build() } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt index 1211224e..7b4f2386 100644 --- a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt @@ -54,7 +54,7 @@ class ConstructorIoSearchTest { mockServer.enqueue(mockResponse) val observer = constructorIo.getSearchResults("corn").test() observer.assertComplete().assertValue { - it.get()!!.searchData.searchResults!!.size == 20 + it.get()!!.searchData.searchResults!!.size == 24 } val request = mockServer.takeRequest() val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt=" diff --git a/library/src/test/resources/search_response.json b/library/src/test/resources/search_response.json index 1cce0cbe..ebc0058e 100755 --- a/library/src/test/resources/search_response.json +++ b/library/src/test/resources/search_response.json @@ -1,598 +1,1383 @@ { "request": { - "ef-11": "22", - "ef-ab": "cd", + "feature_variants": { + "auto_generated_refined_query_rules": null, + "manual_searchandizing": null, + "personalization": null, + "query_items": null + }, + "features": { + "auto_generated_refined_query_rules": true, + "manual_searchandizing": true, + "personalization": true, + "query_items": true + }, "fmt_options": { "groups_max_depth": 1, "groups_start": "current" }, - "num_results_per_page": 20, + "num_results_per_page": 24, "page": 1, + "searchandized_items": {}, "section": "Products", "sort_by": "relevance", "sort_order": "descending", "term": "corn" }, "response": { - "facets": [], - "groups": [ - { - "children": [], - "count": 9, - "display_name": "Horror", - "group_id": "27", - "parents": [] - }, - { - "children": [], + "facets": [{ + "display_name": "Brand", + "name": "Brand", + "options": [{ + "count": 5, + "data": {}, + "display_name": "Del Monte", + "status": "", + "value": "Del Monte" + }, { + "count": 22, + "data": {}, + "display_name": "Signature Kitchens", + "status": "", + "value": "Signature Kitchens" + }, { + "count": 2, + "data": {}, + "display_name": "Signature Farms", + "status": "", + "value": "Signature Farms" + }, { + "count": 10, + "data": {}, + "display_name": "O Organics", + "status": "", + "value": "O Organics" + }, { "count": 7, - "display_name": "Thriller", - "group_id": "53", - "parents": [] - }, - { - "children": [], + "data": {}, + "display_name": "Signature Select", + "status": "", + "value": "Signature Select" + }, { "count": 4, - "display_name": "Drama", - "group_id": "18", - "parents": [] - }, - { - "children": [], + "data": {}, + "display_name": "Seneca Foods", + "status": "", + "value": "Seneca Foods" + }, { + "count": 19, + "data": {}, + "display_name": "General Mills", + "status": "", + "value": "General Mills" + }, { + "count": 5, + "data": {}, + "display_name": "Kelloggs", + "status": "", + "value": "Kelloggs" + }, { + "count": 1, + "data": {}, + "display_name": "Signature Cafe", + "status": "", + "value": "Signature Cafe" + }, { + "count": 1, + "data": {}, + "display_name": "Birds Eye", + "status": "", + "value": "Birds Eye" + }, { + "count": 1, + "data": {}, + "display_name": "Birds Eye Foods", + "status": "", + "value": "Birds Eye Foods" + }, { + "count": 7, + "data": {}, + "display_name": "Mission", + "status": "", + "value": "Mission" + }, { + "count": 4, + "data": {}, + "display_name": "Campbell Soup", + "status": "", + "value": "Campbell Soup" + }, { + "count": 8, + "data": {}, + "display_name": "Frito Lay", + "status": "", + "value": "Frito Lay" + }, { + "count": 1, + "data": {}, + "display_name": "Green Valley Foods", + "status": "", + "value": "Green Valley Foods" + }, { + "count": 1, + "data": {}, + "display_name": "Hormel Foods", + "status": "", + "value": "Hormel Foods" + }, { + "count": 5, + "data": {}, + "display_name": "Foster Farms", + "status": "", + "value": "Foster Farms" + }, { + "count": 8, + "data": {}, + "display_name": "Gerber", + "status": "", + "value": "Gerber" + }, { + "count": 10, + "data": {}, + "display_name": "ConAgra Foods", + "status": "", + "value": "ConAgra Foods" + }, { "count": 3, - "display_name": "Documentary", - "group_id": "99", - "parents": [] - }, - { - "children": [], + "data": {}, + "display_name": "Gruma", + "status": "", + "value": "Gruma" + }, { + "count": 6, + "data": {}, + "display_name": "Guerrero", + "status": "", + "value": "Guerrero" + }, { "count": 1, - "display_name": "Mystery", - "group_id": "9648", - "parents": [] - }, - { - "children": [], + "data": {}, + "display_name": "Jif", + "status": "", + "value": "Jif" + }, { "count": 1, - "display_name": "Fantasy", - "group_id": "14", - "parents": [] - }, - { + "data": {}, + "display_name": "Quaker Oats", + "status": "", + "value": "Quaker Oats" + }, { + "count": 2, + "data": {}, + "display_name": "Pirate Brands", + "status": "", + "value": "Pirate Brands" + }, { + "count": 1, + "data": {}, + "display_name": "Hormel", + "status": "", + "value": "Hormel" + }, { + "count": 1, + "data": {}, + "display_name": "Angies", + "status": "", + "value": "Angies" + }, { + "count": 6, + "data": {}, + "display_name": "Frito-Lay, INC", + "status": "", + "value": "Frito-Lay, INC" + }, { + "count": 1, + "data": {}, + "display_name": "Jose Ole", + "status": "", + "value": "Jose Ole" + }, { + "count": 4, + "data": {}, + "display_name": "ACH Food", + "status": "", + "value": "ACH Food" + }, { + "count": 3, + "data": {}, + "display_name": "B & G Foods", + "status": "", + "value": "B & G Foods" + }, { + "count": 1, + "data": {}, + "display_name": "Bar-S", + "status": "", + "value": "Bar-S" + }, { + "count": 1, + "data": {}, + "display_name": "Tupman Thurlow", + "status": "", + "value": "Tupman Thurlow" + }, { + "count": 1, + "data": {}, + "display_name": "Plum", + "status": "", + "value": "Plum" + }, { + "count": 3, + "data": {}, + "display_name": "Beechnut", + "status": "", + "value": "Beechnut" + }, { + "count": 1, + "data": {}, + "display_name": "Sampco", + "status": "", + "value": "Sampco" + }, { + "count": 1, + "data": {}, + "display_name": "Seeds Of Change", + "status": "", + "value": "Seeds Of Change" + }, { + "count": 1, + "data": {}, + "display_name": "World Finer Foods", + "status": "", + "value": "World Finer Foods" + }, { + "count": 3, + "data": {}, + "display_name": "Hillshire Brands", + "status": "", + "value": "Hillshire Brands" + }, { + "count": 1, + "data": {}, + "display_name": "Vigo Importing", + "status": "", + "value": "Vigo Importing" + }, { + "count": 1, + "data": {}, + "display_name": "PEPPERIDGE FARM", + "status": "", + "value": "PEPPERIDGE FARM" + }, { + "count": 2, + "data": {}, + "display_name": "Tostitos", + "status": "", + "value": "Tostitos" + }, { + "count": 3, + "data": {}, + "display_name": "La Tortilla Factory", + "status": "", + "value": "La Tortilla Factory" + }, { + "count": 2, + "data": {}, + "display_name": "Marie Callenders", + "status": "", + "value": "Marie Callenders" + }, { + "count": 2, + "data": {}, + "display_name": "Ortega", + "status": "", + "value": "Ortega" + }, { + "count": 1, + "data": {}, + "display_name": "American Pop Corn", + "status": "", + "value": "American Pop Corn" + }, { + "count": 4, + "data": {}, + "display_name": "Quaker", + "status": "", + "value": "Quaker" + }, { + "count": 1, + "data": {}, + "display_name": "JFC International", + "status": "", + "value": "JFC International" + }, { + "count": 1, + "data": {}, + "display_name": "Smartfood", + "status": "", + "value": "Smartfood" + }, { + "count": 3, + "data": {}, + "display_name": "Ferrara Candy", + "status": "", + "value": "Ferrara Candy" + }, { + "count": 3, + "data": {}, + "display_name": "PIC", + "status": "", + "value": "PIC" + }, { + "count": 1, + "data": {}, + "display_name": "Azteca Foods", + "status": "", + "value": "Azteca Foods" + }, { + "count": 1, + "data": {}, + "display_name": "Ole Mexican", + "status": "", + "value": "Ole Mexican" + }, { + "count": 1, + "data": {}, + "display_name": "Old El Paso", + "status": "", + "value": "Old El Paso" + }, { + "count": 2, + "data": {}, + "display_name": "Late July Snacks", + "status": "", + "value": "Late July Snacks" + }, { + "count": 1, + "data": {}, + "display_name": "Mondelez", + "status": "", + "value": "Mondelez" + }, { + "count": 1, + "data": {}, + "display_name": "Circle Foods", + "status": "", + "value": "Circle Foods" + }, { + "count": 2, + "data": {}, + "display_name": "Kraft Foods", + "status": "", + "value": "Kraft Foods" + }, { + "count": 1, + "data": {}, + "display_name": "Famous Products", + "status": "", + "value": "Famous Products" + }, { + "count": 1, + "data": {}, + "display_name": "Popcorn Indiana", + "status": "", + "value": "Popcorn Indiana" + }, { + "count": 2, + "data": {}, + "display_name": "RW Garcia", + "status": "", + "value": "RW Garcia" + }, { + "count": 1, + "data": {}, + "display_name": "Cornfields", + "status": "", + "value": "Cornfields" + }, { + "count": 2, + "data": {}, + "display_name": "American Popcorn", + "status": "", + "value": "American Popcorn" + }, { + "count": 1, + "data": {}, + "display_name": "Logan", + "status": "", + "value": "Logan" + }, { + "count": 1, + "data": {}, + "display_name": "Bobs", + "status": "", + "value": "Bobs" + }, { + "count": 2, + "data": {}, + "display_name": "Clabber Girl", + "status": "", + "value": "Clabber Girl" + }, { + "count": 1, + "data": {}, + "display_name": "Aunt Jemima", + "status": "", + "value": "Aunt Jemima" + }], + "type": "multiple" + }, { + "display_name": "Nutrition", + "name": "Nutrition", + "options": [{ + "count": 135, + "data": null, + "display_name": "Kosher", + "status": "", + "value": "Kosher" + }, { + "count": 20, + "data": {}, + "display_name": "Low Fat", + "status": "", + "value": "Low Fat" + }, { + "count": 23, + "data": {}, + "display_name": "Organic", + "status": "", + "value": "Organic" + }, { + "count": 70, + "data": {}, + "display_name": "Gluten Free", + "status": "", + "value": "Gluten Free" + }, { + "count": 6, + "data": {}, + "display_name": "Fat Free", + "status": "", + "value": "Fat Free" + }], + "type": "multiple" + }, { + "display_name": "Price", + "max": 9.09, + "min": 0.69, + "name": "Price", + "status": {}, + "type": "range" + }], + "features": [{ + "display_name": "Affinity Engine", + "enabled": true, + "feature_name": "auto_generated_refined_query_rules", + "variant": null + }, { + "display_name": "Searchandizing", + "enabled": true, + "feature_name": "manual_searchandizing", + "variant": null + }, { + "display_name": "Personalization", + "enabled": true, + "feature_name": "personalization", + "variant": null + }, { + "display_name": "Learn To Rank", + "enabled": true, + "feature_name": "query_items", + "variant": null + }], + "groups": [{ + "children": [{ + "children": [], + "count": 38, + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { "children": [], "count": 1, - "display_name": "Animation", - "group_id": "16", - "parents": [] - }, - { + "display_name": "Fruits & Vegetables", + "group_id": "Fruits%20%26%20Vegetables", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { + "children": [], + "count": 17, + "display_name": "Breakfast & Cereal", + "group_id": "Breakfast%20%26%20Cereal", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { + "children": [], + "count": 2, + "display_name": "Deli", + "group_id": "Deli", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { + "children": [], + "count": 26, + "display_name": "Frozen Foods", + "group_id": "Frozen%20Foods", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { + "children": [], + "count": 28, + "display_name": "International Cuisine", + "group_id": "International%20Cuisine", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { + "children": [], + "count": 71, + "display_name": "Cookies, Snacks & Candy", + "group_id": "Cookies%2C%20Snacks%20%26%20Candy", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { + "children": [], + "count": 13, + "display_name": "Baby Care", + "group_id": "Baby%20Care", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { + "children": [], + "count": 19, + "display_name": "Condiments, Spice & Bake", + "group_id": "Condiments%2C%20Spice%20%26%20Bake", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { "children": [], "count": 6, - "display_name": "Comedy", - "group_id": "35", - "parents": [] - }, - { + "display_name": "Meat & Seafood", + "group_id": "Meat%20%26%20Seafood", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }, { "children": [], - "count": 3, - "display_name": "Crime", - "group_id": "80", - "parents": [] + "count": 4, + "display_name": "Grains, Pasta & Sides", + "group_id": "Grains%2C%20Pasta%20%26%20Sides", + "parents": [{ + "display_name": "All", + "group_id": "all" + }] + }], + "count": 225, + "display_name": "All", + "group_id": "all", + "parents": [] + }], + "results": [{ + "data": { + "description": "Made with fresh cut golden sweet whole kernel corn. No preservatives. Non GMO (Corn used in this product is not genetically modified or bioengineered). Quality. USDA Process verified (USDA Process verified non-GE/GMO http://processverified.usda.gov). Visit Us at: www.delmonte.com. Questions or comments? Call 1-800-543-3090 (Mon-Fri). Please provide code information from the end of can when calling or writing. Please recycle. 1 g total fat (Amount in undrained [1/2 cup]). 320 mg sodium (210 mg sodium when drained). Grown in the USA. Product of USA.", + "facets": [{ + "name": "Brand", + "values": ["Del Monte"] + }, { + "name": "Price", + "values": [2.29] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150086", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150086.png", + "keywords": ["corn"], + "price": 2.29, + "url": "/", + "weighted_keywords": { + "sweet": 2 + } }, - { - "children": [], - "count": 3, - "display_name": "Action", - "group_id": "28", - "parents": [] + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Del Monte Fresh Cut Corn Whole Kernel Golden Sweet with Natural Sea Salt - 15.25 Oz" + }, { + "data": { + "description": "Per 1/2 cup: 80 calories; 0 g sat fat (0% DV); 300 mg sodium (13% DV); 7 g total sugars. Quality guaranteed. Non BPA lining (can lining produced without the intentional additional of BPA). Our Signature is Our Promise: Signature Select is your assurance of great quality products at the best value everyday. We source freshly picked fruits and vegetables at the peak of each season. We wouldn't put our Signature on anything else. Quality and satisfaction guaranteed or your money back. Our Promise: Quality & satisfaction 100% guaranteed or your money back. www.betterlivingbrandsLLC.com. Smartlabel.", + "facets": [{ + "name": "Brand", + "values": ["Signature Kitchens"] + }, { + "name": "Nutrition", + "values": ["Kosher", "Low Fat"] + }, { + "name": "Price", + "values": [1.3] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150012", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150012.png", + "keywords": ["corn"], + "price": 1.3, + "url": "/", + "weighted_keywords": { + "sweet": 2 + } }, - { - "children": [], - "count": 2, - "display_name": "Romance", - "group_id": "10749", - "parents": [] + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Kitchens Corn Whole Kernel Golden Sweet Can - 15.25 Oz" + }, { + "data": { + "description": "Made with Fresh Cut Golden Sweet Whole Kernel Corn. Non GMO (Corn used in this product is not genetically modified or bioengineered). NO preservatives. Quality. USDA Process Verified: USDA Process Verified Non-GE/GMO http://processverified.usda.gov. Visit Us at: www.delmonte.com. Questions or comments? Call 1-800-543-3090 (Mon-Fri). Please provide code information from the end of can when calling or writing. Please recycle. Not a sodium-free food. Grown in the USA. Product of USA.", + "facets": [{ + "name": "Brand", + "values": ["Del Monte"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [2.29] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150013", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150013.png", + "keywords": ["corn"], + "price": 2.29, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Del Monte Fresh Cut Corn Whole Kernel Golden Sweet No Salt Added - 15.25 Oz" + }, { + "data": { + "description": "No salt added. Quality guaranteed. Our Signature is our Promise: Signature Select is your assurance of great quality products at the best value everyday. We source freshly picked Scores and vegetables at the peak of each season. We wouldn't put our Signature on anything else Quality and satisfaction guaranteed or your money back. Our Promise: Quality & satisfaction 100% guaranteed or your money back. Non BPA lining (can lining produced without the intentional addition of BPA). Not a sodium free food. Per 1/2 Cup: 60 calories; 0 g sat fat (0% DV); 10 mg sodium (0% DV); 2 g total sugars. Not a sodium free food. www.betterlivingbrandsLLC.com. SmartLabel: can for more food information.", + "facets": [{ + "name": "Brand", + "values": ["Signature Kitchens"] + }, { + "name": "Nutrition", + "values": ["Kosher", "Low Fat"] + }, { + "name": "Price", + "values": [1.3] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150048", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150048.png", + "keywords": ["corn"], + "price": 1.3, + "url": "/", + "weighted_keywords": { + "add": 2, + "sweet": 2 + } + }, + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Kitchens Corn Whole Kernel Golden Sweet Not Salt Added Can - 15.25 Oz" + }, { + "data": { + "description": "Quality guaranteed. Delicious Quality from Farm to Table: Welcome to the extraordinary flavors, textures, and colors of Signature Farms. You can find all the produce you want under the sun - crispy greens, perfectly ripened fruit, and all the colors of the vegetable rainbow. www.betterlivingbrandsLLC.com. SmartLabel. Scan for more food information. Product of Mexico.", + "facets": [{ + "name": "Brand", + "values": ["Signature Farms"] + }, { + "name": "Price", + "values": [5.9] + }], + "groups": [{ + "display_name": "Fruits & Vegetables", + "group_id": "Fruits%20%26%20Vegetables", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960111073", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960111073.png", + "keywords": ["vegetables", "packaged"], + "price": 5.9, + "url": "/", + "weighted_keywords": { + "sweet": 2 + } + }, + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Farms Corn Super Sweet - 4 Count" + }, { + "data": { + "description": "Per 1/2 Cup: 60 calories; 0 g sat fat (0% DV); 130 mg sodium (6% DV); 2 g total sugars. USDA Organic. Organic from the Source: Doesn't it feel good to know where your food comes from? At O Organics, we carefully select ingredients which meet organic farming standards and share our commitment to organic agriculture. That's our promise. Quality & satisfaction 100% guaranteed or your money back. SmartLabel: Scan for more food information. Non BPA lining (Can lining produced without the intentional addition of BPA).", + "facets": [{ + "name": "Brand", + "values": ["O Organics"] + }, { + "name": "Nutrition", + "values": ["Kosher", "Organic"] + }, { + "name": "Price", + "values": [2.09] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150017", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150017.png", + "keywords": ["corn"], + "price": 2.09, + "url": "/", + "weighted_keywords": { + "organic": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "O Organics Organic Corn Whole Kernel - 15.25 Oz" + }, { + "data": { + "description": "Made with fresh cut golden sweet corn cream style. With natural sea salt. Quality. No preservatives. Non GMO (Ingredients of the types used in this product are not genetically modified). Non-BPA (Packaging produced without the intentional addition of BPA). Questions or comments? Call 1-800-543-3090 (Mon.-Fri.). Please provide code information from the end of can when calling or writing. Please recycle.", + "facets": [{ + "name": "Brand", + "values": ["Del Monte"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [2.29] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150085", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150085.png", + "keywords": ["corn"], + "price": 2.29, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Del Monte Fresh Cut Corn Cream Style Golden Sweet - 14.75 Oz" + }, { + "data": { + "description": "Quality guaranteed. Our Promise: Quality & satisfaction 100% guaranteed or your money back. Non BPA lining (can lining produced without the intentional addition of BPA). Per 1/2 Cup: 70 calories; 0 g sat fat (0% DV); 300 mg sodium (13% DV); 8 g total sugars. Our Signature is Our Promise: Signature Select is your assurance of great quality products at the best value everyday. We wouldn't put our signature on anything else. Quality and satisfaction guaranteed or your money back. www.betterlivingbrandsLLC.com. SmartLabel: Scan for more food information.", + "facets": [{ + "name": "Brand", + "values": ["Signature Select"] + }, { + "name": "Nutrition", + "values": ["Kosher", "Low Fat"] + }, { + "name": "Price", + "values": [1.3] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150010", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150010.png", + "keywords": ["corn"], + "price": 1.3, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Select Corn Golden Sweet Cream Style - 14.75 Oz" + }, { + "data": { + "description": "Quality guaranteed. Picked & packed at the peak of freshness. Per 1/2 Cup: 80 calories. At Signature Kitchens we're passionate about quality ingredients - like our corn. We source freshly picked fruits and vegetables at the peak of each season. www.betterlivingbrandsLLC.com. Our promise, quality & satisfaction 100% guaranteed or your money back. See end of can for country of orgin.", + "facets": [{ + "name": "Brand", + "values": ["Signature Kitchens"] + }, { + "name": "Nutrition", + "values": ["Kosher", "Low Fat"] + }, { + "name": "Price", + "values": [0.98] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121050023", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121050023.png", + "keywords": ["corn"], + "price": 0.98, + "url": "/", + "weighted_keywords": { + "sweet": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Kitchens Corn Whole Kernel Golden Sweet Can - 8.5 Oz" + }, { + "data": { + "description": "Made with farm fresh goodness! Visit our website at www.senecafoods.com. Please recycle.", + "facets": [{ + "name": "Brand", + "values": ["Seneca Foods"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [1.11] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960096944", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960096944.png", + "keywords": ["corn"], + "price": 1.11, + "url": "/", + "weighted_keywords": { + "sweet": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Libbys Corn Whole Kernel Sweet - 15 Oz" + }, { + "data": { + "description": "Naturally flavored frosted corn puffs. Cuckoo for chocolatey milk! Per 3/4 Cup Serving: 100 calories; 0 g sat fat (0% DV); 100 mg sodium (4% DV); 9 g sugars. See nutrition facts for As Prepared information. 10 g whole grain per serving. At least 48 g recommended daily. Whole grain 1st ingredient in every General Mills Big G cereal. First ingredient whole grain. A whole grain food is made by using all three parts of grain. All General Mills big g cereals contain more whole grain than any other single ingredient. Produced with genetic engineering. Box Tops for Education. We serve the world by making food people love. We welcome your questions and comments generallmills.com. Exchange: 1-1/2 starch. Based on Academy of Nutrition and Diabetics and American Diabetes Association criteria. This package is sold by weight, not volume. You can be assured of proper weight even though some settling of contents normally occurs during shipment and handling. Learn more at ask.generalmills.com. how2rec", + "facets": [{ + "name": "Brand", + "values": ["General Mills"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [4.99] + }], + "groups": [{ + "display_name": "Breakfast & Cereal", + "group_id": "Breakfast%20%26%20Cereal", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960161250", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960161250.png", + "keywords": ["sweet", "cereal"], + "price": 4.99, + "url": "/", + "weighted_keywords": { + "size": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Cocoa Puffs Frosted Corn Puffs Family Size - 20.9 Oz" + }, { + "data": { + "description": "Box Tops For Education. Endorsed by Weight Watchers. 2 PointsPlus value per serving. 100 calories per serving. Steam Crisp corn is vacuum packed and perfectly steam-cooked in the can. It contains the same amount of product as our standard can of corn, but uses less water and packaging, so it's friendlier on the environment too. Please recycle. Better if used by date on end of can. Weight Watchers PointsPlus Program! Check out the Weight Watchers PointsPlus Program. To learn more visit WeightWatchers.com or call 1(800)410-1199 today. A gluten free food. See how we lock in freshness at www.GreenGiant.com.", + "facets": [{ + "name": "Brand", + "values": ["General Mills"] + }, { + "name": "Nutrition", + "values": ["Gluten Free", "Kosher"] + }, { + "name": "Price", + "values": [1.67] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150003", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150003.png", + "keywords": ["corn"], + "price": 1.67, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Green Giant SteamCrisp Corn Whole Kernel White Shoepeg - 11 Oz" + }, { + "data": { + "description": "Picked at the peak of perfection. Endorsed by Weight Watchers. 1 PointsPlus value per serving. 60 calories per serving. Box Tops for Education. Our love for vegetables began over 100 years ago in Le Sueur, Minnesota. Today, we still have fourth generation farmers who pick each crop at the peak of perfection. That's how we make our vegetables unforgettable. That's Green Giant. Please recycle. Weight Watchers PointsPlus Program! Check out the Weight Watchers PointsPlus Program. To learn more visit WeightWatchers.com or call 1 (800) 410-1199 today. See how we lock in freshness at www.GreenGiant.com.", + "facets": [{ + "name": "Brand", + "values": ["General Mills"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [1.25] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "121150011", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150011.png", + "keywords": ["corn"], + "price": 1.25, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Green Giant Corn Whole Kernel Sweet - 15.25 Oz" + }, { + "data": { + "description": "Sweet crispy crunch! Per 1 Cup Serving: 120 calories; 0 g sat fat (0% DV); 105 mg sodium (4% DV); 9 g sugars. Kellogg's Family Rewards. Learn more at kfr.com. Collect points. Earn rewards. Two easy ways to collect points! Go to kfr.com to learn more. USA Olympic proud sponsor. Kellogg's Open for Breakfast. Let's talk. At Kellogg, we're working harder to earn a seat at your table. What can we do to make your mornings better? Questions or comments? Visit: kelloggs.com. Call: 1-800-962-1413. Provide production code on package. BCTGM: Bakery Confectionery Tobacco Workers & Grain Millers union made. AFL CIO CLC. Certified 100% recycled paperboard. kelloggs.com. how2recycle.info.", + "facets": [{ + "name": "Brand", + "values": ["Kelloggs"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [5.69] + }], + "groups": [{ + "display_name": "Breakfast & Cereal", + "group_id": "Breakfast%20%26%20Cereal", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960015388", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960015388.png", + "keywords": ["sweet", "cereal"], + "price": 5.69, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Kelloggs Corn Pops Cereal Sweetened - 12.5 Oz" + }, { + "data": { + "description": "Quality guaranteed. Our Promise: Quality & satisfaction 100% guaranteed or your money back. Non BPA lining (can lining produced without the intentional addition of BPA). Per 1/3 Cup: 60 calories; 0 g sat fat (0% DV); 230 mg sodium (10% DV); 5 g total sugars. www.betterlivingbrandsLLC.com. SmartLabel: Scan for more food information.", + "facets": [{ + "name": "Brand", + "values": ["Signature Kitchens"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [1.09] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960009115", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960009115.png", + "keywords": ["corn"], + "price": 1.09, + "url": "/", + "weighted_keywords": { + "sweet": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Kitchens Corn Golden Sweet with Red & Green Peppers Can - 11 Oz" + }, { + "data": { + "description": "Rich & creamy. Chicken raised without antibiotics. Per 1 Cup: 220 calories; 6 g sat fat (30% DV); 710 mg sodium (30% DV); 6 g sugars. Inspected for wholesomeness by US Department of Agriculture. Smartlabel. Scan for more food information. Quality & satisfaction guaranteed or your money back.", + "facets": [{ + "name": "Brand", + "values": ["Signature Cafe"] + }, { + "name": "Price", + "values": [5.69] + }], + "groups": [{ + "display_name": "Deli", + "group_id": "Deli", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960071695", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960071695.png", + "keywords": ["deli", "soups"], + "price": 5.69, + "url": "/", + "weighted_keywords": { + "chicken": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Cafe Soup Chicken & Sweet Corn Chowder - 24 Oz" + }, { + "data": { + "description": "Per 1 Cup Serving: 190 calories; 2 g sat fat (9% DV); 870 mg sodium (36% DV); 5 g sugars. Inspected for wholesomeness by US Department of Agriculture. All white meat chicken with no added antibiotics. Questions, comments? Save can and call 1-800-200-9377 weekdays 7:30 AM to 5:30 PM CT. Box Tops for Education. Gluten free. Visit our website at www.Progresso.com. how2recycle.info. For 100 years, our kitchens have crafted honest, soul-satisfying recipes with real ingredients you're proud to serve. Visit progresso.com for even more coziness. No artificial flavors. No MSG added (except that which occurs naturally in yeast extract hydrolyzed vegetable proteins, and tomato extract.). Exchanges: 1-1/2 starch, 1 lean meat, 1 fat. Based on Academy of Nutrition and Dietetics and American Diabetes Association criteria. Made in the USA.", + "facets": [{ + "name": "Brand", + "values": ["General Mills"] + }, { + "name": "Nutrition", + "values": ["Gluten Free"] + }, { + "name": "Price", + "values": [3.39] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "125300232", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-125300232.png", + "keywords": ["ready", "to", "serve", "soup"], + "price": 3.39, + "url": "/", + "weighted_keywords": { + "rich": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Progresso Rich & Hearty Soup Chicken Corn Chowder Flavored with Bacon - 18.5 Oz" + }, { + "data": { + "description": "Selects. Fresh frozen vegetables. No preservatives. Perfectly cooks in the bag! Questions or comments? 800-563-1786 M-F 9:00am - 5:00pm EST. www.birdseye.com. Product of USA.", + "facets": [{ + "name": "Brand", + "values": ["Birds Eye"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [2.89] + }], + "groups": [{ + "display_name": "Frozen Foods", + "group_id": "Frozen%20Foods", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960143197", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960143197.png", + "keywords": ["frozen", "vegetables"], + "price": 2.89, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Birds Eye Steamfresh Selects Super Sweet Corn Bag - 10 Oz" + }, { + "data": { + "description": "Per 1 Cup Serving: 100 calories; 0 g sat fat (0% DV); 200 mg sodium (8% DV); 3 g sugar. Kellogg's Family Rewards. Learn more at kfr.com. Collect points. Earn rewards. Two easy ways to collect points! Go to kfr.com to learn more. USA Olympic Proud Sponsor. The original & best. A Tradition of Simple Goodness: More than 100 years ago, Kellogg recognized the possibilities in a single grain. And with the simple goodness of our toasted corn flakes, breakfast cereal was born. We've been baking Kellogg's Corn Flakes with the same simple recipe ever since. Kellogg's Open for breakfast. OpenForBreakfast.com/Cornflakes. Made using a simple recipe for more than 100 years. The corn used to make Kellogg's Corn Flakes is US grown. Split kernels of corn are cooked & partially dried. Rolled thin into flakes, they're ready for toasting. From seed to spoon, you get our best in every bite. Learn more at OpenForBreakfast.com/Cornflakes. Cornelius. Fat free. Produced with genetic engineering. Questions or c", + "facets": [{ + "name": "Brand", + "values": ["Kelloggs"] + }, { + "name": "Nutrition", + "values": ["Fat Free", "Kosher"] + }, { + "name": "Price", + "values": [6.89] + }], + "groups": [{ + "display_name": "Breakfast & Cereal", + "group_id": "Breakfast%20%26%20Cereal", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "111010115", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-111010115.png", + "keywords": ["all", "family", "cereal"], + "price": 6.89, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Kelloggs Corn Flakes Cereal - 18 Oz" + }, { + "data": { + "description": "Natural fruit flavored sweetened corn puffs with other natural flavors. No high fructose corn syrup. No colors from artificial sources. No artificial flavors. Per 1 Cup Serving: 130 calories; 0 g sat fat (0% DV); 160 mg sodium (7% DV); 10 g sugars. See nutrition facts for as prepared information. First ingredient whole grain. A whole grain food is made by using all three parts of the grain. All general mills big g cereals contain more whole grain than any other single ingredient. Box Tops for Education. We are on a journey to make cereal better. We welcome your questions and comments. Generalmills.com. 1-800-328-114. Welcome to Satur-Yay-Aaah!! There's a bit of Saturday in every box of Trix, Cocoa Puffs, Golden Grahams, Cookie Crisp and REese's Puffs cereals. Pour a bowl of your favorite to make any weekday feel like Saturday! To add even more Satur-Yay-Aaah fun to your day. Try out one of these crazy hairstyles: Get up early and take golden opportunity to read your favorite comics. Ma", + "facets": [{ + "name": "Brand", + "values": ["General Mills"] + }, { + "name": "Price", + "values": [2.5] + }], + "groups": [{ + "display_name": "Breakfast & Cereal", + "group_id": "Breakfast%20%26%20Cereal", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "111011133", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-111011133.png", + "keywords": ["sweet", "cereal"], + "price": 2.5, + "url": "/", + "weighted_keywords": { + "sweeten": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Trix Cereal Sweetened Corn Puffs - 10.7 Oz" + }, { + "data": { + "description": "Naturally flavored. Made with real honey. Sweetened corn cereal with a touch of real honey and natural almond flavor. Gluten free. No high fructose corn syrup. No artificial flavors, colors or preservatives. Per 3/4 Cup Serving: 120 calories; 0 g sat fat (0% DV); 200 mg sodium (8% DV); 9 g sugars. See nutrition facts for as prepared information. Whole grain is the 1st ingredient. First ingredient whole grain. A whole grain food is made by using all three parts of the grain. All General Mills Big G cereals contain more whole grain than any other single ingredient. 10 g whole grain per serving. At least 48 g recommended daily. Produced with genetic engineering. Learn more at Ask.GeneralMills.com. Taste the possibilities in every square. Chex Vanilla; Chex Corn; Chex Rice; Chex Chocolate; Chex Blueberry; Chex Cinnamon; Chex Wheat. Make a splash with the great taste of honey in your bowl! Proud sponsor of Celiac Disease Foundation. Celiac.org. Exchange: 1-1/2 starch. Based on Academy of Nu", + "facets": [{ + "name": "Brand", + "values": ["General Mills"] + }, { + "name": "Nutrition", + "values": ["Gluten Free", "Kosher"] + }, { + "name": "Price", + "values": [2.5] + }], + "groups": [{ + "display_name": "Breakfast & Cereal", + "group_id": "Breakfast%20%26%20Cereal", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960111322", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960111322.png", + "keywords": ["all", "family", "cereal"], + "price": 2.5, + "url": "/", + "weighted_keywords": { + "nut": 2, + "sweetend": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Chex Cereal Corn Gluten Free Sweetend Honey Nut - 12.5 Oz" + }, { + "data": { + "description": "Premium quality. California & Washington. Visit our website at www.birdseye.com. At Birds Eye, we believe passionately in quality and that quality of life, family and friendships are the only reasons for our existence. We also believe that our mission is to bring our customers only the very best quality of product, of package, and of service. That is our heritage and that is our future. Our pledge is to offer our customers the very best grown and instantly quick frozen vegetables and fruit, and to continuously improve the choice of products that we offer you. Thank you for choosing Birds Eye products. You will always get our very best since quality is our business. Product of USA.", + "facets": [{ + "name": "Brand", + "values": ["Birds Eye Foods"] + }, { + "name": "Nutrition", + "values": ["Kosher"] + }, { + "name": "Price", + "values": [3.6] + }], + "groups": [{ + "display_name": "Frozen Foods", + "group_id": "Frozen%20Foods", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "147010091", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-147010091.png", + "keywords": ["frozen", "vegetables"], + "price": 3.6, + "url": "/" }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "C&W Petite Sweet Corn - 16 Oz" + }, { + "data": { + "description": "Quality guaranteed. A low fat food. Per 1 Ear: 90 calories; 0 g sat fat (0% DV); 0 mg sodium (0% DV); 5 g sugars. Package contains 12 mini ears. www.betterlivingbrandsLLC.com. Our Promise: Quality & satisfaction 100% guaranteed or your money back. Product of USA.", + "facets": [{ + "name": "Brand", + "values": ["Signature Kitchens"] + }, { + "name": "Nutrition", + "values": ["Kosher", "Low Fat"] + }, { + "name": "Price", + "values": [5.19] + }], + "groups": [{ + "display_name": "Frozen Foods", + "group_id": "Frozen%20Foods", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "147010663", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-147010663.png", + "keywords": ["frozen", "vegetables"], + "price": 5.19, + "url": "/", + "weighted_keywords": { + "corn": 2 + } }, - { - "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" + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Kitchens Corn On The Cob Mini - 12 Count" + }, { + "data": { + "description": "Quality guaranteed. Our Promise: Quality & satisfaction 100% guaranteed or your money back. Non BPA lining (can lining produced without the intentional addition of BPA). Per 1/2 Cup: 60 calories; 0 g sat fat (0% DV); 200 mg sodium (9% DV); 2 g total sugars. www.betterlivingbrandsLLC.com. SmartLabel: Scan for more food information.", + "facets": [{ + "name": "Brand", + "values": ["Signature Kitchens"] + }, { + "name": "Nutrition", + "values": ["Gluten Free", "Kosher"] + }, { + "name": "Price", + "values": [1.11] + }], + "groups": [{ + "display_name": "Canned Goods & Soups", + "group_id": "Canned%20Goods%20%26%20Soups", + "path": "/all", + "path_list": [{ + "display_name": "All", + "id": "all" + }] + }], + "id": "960197607", + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960197607.png", + "keywords": ["corn"], + "price": 1.11, + "url": "/", + "weighted_keywords": { + "sweet": 2 + } }, - { - "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" - } - ], + "is_slotted": false, + "matched_terms": ["corn"], + "value": "Signature Kitchens Corn Whole Kernel Super Sweet - 15.25 Oz" + }], "sort_options": [], - "total_num_results": 23 + "total_num_results": 225 }, - "result_id": "5b19c365-1c85-4c2d-b627-0509d60ce2d5" -} + "result_id": "49eb05c2-a3a3-4080-b238-82c5a44f8cf5" +} \ No newline at end of file diff --git a/library/src/test/resources/search_response_empty.json b/library/src/test/resources/search_response_empty.json index 5f82e2cc..3d804c9d 100644 --- a/library/src/test/resources/search_response_empty.json +++ b/library/src/test/resources/search_response_empty.json @@ -1,17 +1,28 @@ { "request": { - "ef-11": "22", - "ef-ab": "cd", + "feature_variants": { + "auto_generated_refined_query_rules": null, + "manual_searchandizing": null, + "personalization": null, + "query_items": null + }, + "features": { + "auto_generated_refined_query_rules": true, + "manual_searchandizing": true, + "personalization": true, + "query_items": true + }, "fmt_options": { "groups_max_depth": 1, "groups_start": "current" }, - "num_results_per_page": 20, + "num_results_per_page": 24, "page": 1, + "searchandized_items": {}, "section": "Products", "sort_by": "relevance", "sort_order": "descending", - "term": "cornucopiasofcorndogs" + "term": "corn" }, "response": { "facets": [], @@ -20,5 +31,5 @@ "sort_options": [], "total_num_results": 0 }, - "result_id": "2ea93527-91d6-4dfa-86b0-6a0e8158bfd1" -} + "result_id": "49eb05c2-a3a3-4080-b238-82c5a44f8cf5" +} \ No newline at end of file From 86bd42ce4855b5c91d655258acb39c7399b022cf Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Mon, 25 May 2020 13:52:58 -0400 Subject: [PATCH 6/7] Updated the test --- .../io/constructor/core/ConstructorioSearchTest.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt index 7b4f2386..69728423 100644 --- a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt @@ -55,6 +55,16 @@ class ConstructorIoSearchTest { val observer = constructorIo.getSearchResults("corn").test() observer.assertComplete().assertValue { it.get()!!.searchData.searchResults!!.size == 24 + it.get()!!.searchData.searchResults!![0].value == "Del Monte Fresh Cut Corn Whole Kernel Golden Sweet with Natural Sea Salt - 15.25 Oz" + it.get()!!.searchData.searchResults!![0].result.id == "121150086" + it.get()!!.searchData.searchResults!![0].result.imageUrl == "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-121150086.png" + it.get()!!.searchData.searchResults!![0].result.metadata["price"] == 2.29 + it.get()!!.searchData.searchResults!![0].matchedTerms!![0] == "corn" + it.get()!!.searchData.facets!!.size == 3 + it.get()!!.searchData.facets!![0].displayName == "Brand" + it.get()!!.searchData.facets!![0].type == "multiple" + it.get()!!.searchData.groups!!.size == 1 + it.get()!!.searchData.resultCount == 225 } val request = mockServer.takeRequest() val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt=" @@ -108,6 +118,9 @@ class ConstructorIoSearchTest { val observer = constructorIo.getSearchResults("corn").test() observer.assertComplete().assertValue { it.get()!!.searchData.searchResults!!.isEmpty() + it.get()!!.searchData.facets!!.isEmpty() + it.get()!!.searchData.groups!!.isEmpty() + it.get()!!.searchData.resultCount == 0 } val request = mockServer.takeRequest() val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt=" From 001f411a9b8d09b04038f66b89197b48d278294b Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Mon, 25 May 2020 14:59:49 -0400 Subject: [PATCH 7/7] Fixes --- .../core/ConstructorIoAutocompleteTest.kt | 13 - .../core/ConstructorioSearchTest.kt | 13 - .../io/constructor/util/TestDataLoader.kt | 2 - ...omplete_response_with_unexpected_data.json | 520 --------------- .../search_response_unexpected_data.json | 599 ------------------ 5 files changed, 1147 deletions(-) delete mode 100755 library/src/test/resources/autocomplete_response_with_unexpected_data.json delete mode 100755 library/src/test/resources/search_response_unexpected_data.json diff --git a/library/src/test/java/io/constructor/core/ConstructorIoAutocompleteTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoAutocompleteTest.kt index 34a9e66b..acb05f2e 100644 --- a/library/src/test/java/io/constructor/core/ConstructorIoAutocompleteTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoAutocompleteTest.kt @@ -88,19 +88,6 @@ class ConstructorIoAutocompleteTest { assert(request.path.startsWith(path)) } - @Test - fun getAutocompleteResultsWithUnexpectedResponse() { - val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_with_unexpected_data.json")) - mockServer.enqueue(mockResponse) - val observer = constructorIo.getAutocompleteResults("titanic").test() - observer.assertComplete().assertValue { - it.get()!!.isNotEmpty() && it.get()!!.size == 5 - } - val request = mockServer.takeRequest() - val path = "/autocomplete/titanic?key=golden-key&i=guido-the-guid&ui=player-one&s=79&c=cioand-1.3.0&_dt=" - assert(request.path.startsWith(path)) - } - @Test fun getAutocompleteResultsWithEmptyResponse() { val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_empty.json")) diff --git a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt index 69728423..10c0e204 100644 --- a/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorioSearchTest.kt @@ -98,19 +98,6 @@ class ConstructorIoSearchTest { assert(request.path.startsWith(path)) } - @Test - fun getSearchResultsWithUnexpectedResponse() { - val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_unexpected_data.json")) - mockServer.enqueue(mockResponse) - val observer = constructorIo.getSearchResults("corn").test() - observer.assertComplete().assertValue { - it.get()!!.searchData.resultCount == 23 - } - val request = mockServer.takeRequest() - val path = "/search/corn?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-1.3.0&_dt=" - assert(request.path.startsWith(path)) - } - @Test fun getSearchResultsWithEmptyResponse() { val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_empty.json")) diff --git a/library/src/test/java/io/constructor/util/TestDataLoader.kt b/library/src/test/java/io/constructor/util/TestDataLoader.kt index 684130c0..58a6f1ac 100755 --- a/library/src/test/java/io/constructor/util/TestDataLoader.kt +++ b/library/src/test/java/io/constructor/util/TestDataLoader.kt @@ -15,8 +15,6 @@ object TestDataLoader { fun loadResponse() : AutocompleteResult? = loadResult("autocomplete_response.json") - fun loadResponseWithUnexpectedData() : AutocompleteResult? = loadResult("autocomplete_response_with_unexpected_data.json") - fun loadEmptyResponse() : AutocompleteResult? = loadResult("autocomplete_response_empty.json") private fun loadResult(fileName: String): AutocompleteResult? { diff --git a/library/src/test/resources/autocomplete_response_with_unexpected_data.json b/library/src/test/resources/autocomplete_response_with_unexpected_data.json deleted file mode 100755 index 49b88921..00000000 --- a/library/src/test/resources/autocomplete_response_with_unexpected_data.json +++ /dev/null @@ -1,520 +0,0 @@ -{ - "sections": { - "Products": [ - { - "data": { - "groups": [ - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Thriller", - "group_id": "53", - "path": null - } - ], - "id": "raise the titanic", - "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/7cahPJXA1fRXRwqqMVQkX3MTZy3.jpg", - "url": "https://www.top250.tv/movies/24575" - }, - "matched_terms": [ - "titanic" - ], - "value": "Raise the Titanic" - }, - { - "data": { - "groups": [ - { - "display_name": "Documentary", - "group_id": "99", - "path": null - } - ], - "id": "drain the titanic", - "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/2A9OPQHztBsfFRMzintaOhn4Rry.jpg", - "url": "https://www.top250.tv/movies/428950" - }, - "matched_terms": [ - "titanic" - ], - "value": "Drain the Titanic" - }, - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Romance", - "group_id": "10749", - "path": null - }, - { - "display_name": "Thriller", - "group_id": "53", - "path": null - } - ], - "id": "titanic", - "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/kHXEpyfl6zqn8a6YuozZUujufXf.jpg", - "url": "https://www.top250.tv/movies/597" - }, - "matched_terms": [ - "titanic" - ], - "value": "Titanic" - }, - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - } - ], - "id": "titanic town", - "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zfx8YMEG3sR9t4rKYgOD3O0FoPN.jpg", - "url": "https://www.top250.tv/movies/83036" - }, - "matched_terms": [ - "titanic" - ], - "value": "Titanic Town" - }, - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - } - ], - "id": "the chambermaid on the titanic", - "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/jSsKHwtqk0XNdABR2R5wI3grfxV.jpg", - "url": "https://www.top250.tv/movies/115872" - }, - "matched_terms": [ - "titanic" - ], - "value": "The Chambermaid on the Titanic" - } - ], - "Search Suggestions": [ - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Thriller", - "group_id": "53", - "path": null - }, - { - "display_name": "Mystery", - "group_id": "9648", - "path": null - }, - { - "display_name": "Family", - "group_id": "10751", - "path": null - }, - { - "display_name": "Foreign", - "group_id": "10769", - "path": null - } - ] - }, - "matched_terms": [ - "titanic" - ], - "value": "titanics" - }, - { - "data": { - "groups": [ - { - "display_name": "Documentary", - "group_id": "99", - "path": null - } - ] - }, - "matched_terms": [ - "titanic" - ], - "value": "titanica" - }, - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Romance", - "group_id": "10749", - "path": null - }, - { - "display_name": "Thriller", - "group_id": "53", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Documentary", - "group_id": "99", - "path": null - }, - { - "display_name": "History", - "group_id": "36", - "path": null - }, - { - "display_name": "Adventure", - "group_id": "12", - "path": null - } - ] - }, - "matched_terms": [ - "titanic" - ], - "value": "titanic" - }, - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Adventure", - "group_id": "12", - "path": null - }, - { - "display_name": "Fantasy", - "group_id": "14", - "path": null - }, - { - "display_name": "Family", - "group_id": "10751", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Animation", - "group_id": "16", - "path": null - }, - { - "display_name": "Science Fiction", - "group_id": "878", - "path": null - } - ] - }, - "matched_terms": [ - "titans" - ], - "value": "titans" - }, - { - "data": { - "groups": [ - { - "display_name": "Animation", - "group_id": "16", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Science Fiction", - "group_id": "878", - "path": null - }, - { - "display_name": "Family", - "group_id": "10751", - "path": null - }, - { - "display_name": "Adventure", - "group_id": "12", - "path": null - }, - { - "display_name": "Documentary", - "group_id": "99", - "path": null - }, - { - "display_name": "Horror", - "group_id": "27", - "path": null - }, - { - "display_name": "Fantasy", - "group_id": "14", - "path": null - }, - { - "display_name": "Drama", - "group_id": "18", - "path": null - } - ] - }, - "matched_terms": [ - "titan" - ], - "value": "titan" - } - ], - "Search-ish Suggest-ables": [ - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Thriller", - "group_id": "53", - "path": null - }, - { - "display_name": "Mystery", - "group_id": "9648", - "path": null - }, - { - "display_name": "Family", - "group_id": "10751", - "path": null - }, - { - "display_name": "Foreign", - "group_id": "10769", - "path": null - } - ] - }, - "matched_terms": [ - "titanic" - ], - "value": "titanics" - }, - { - "data": { - "groups": [ - { - "display_name": "Documentary", - "group_id": "99", - "path": null - } - ] - }, - "matched_terms": [ - "titanic" - ], - "value": "titanica" - }, - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Romance", - "group_id": "10749", - "path": null - }, - { - "display_name": "Thriller", - "group_id": "53", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Documentary", - "group_id": "99", - "path": null - }, - { - "display_name": "History", - "group_id": "36", - "path": null - }, - { - "display_name": "Adventure", - "group_id": "12", - "path": null - } - ] - }, - "matched_terms": [ - "titanic" - ], - "value": "titanic" - }, - { - "data": { - "groups": [ - { - "display_name": "Drama", - "group_id": "18", - "path": null - }, - { - "display_name": "Adventure", - "group_id": "12", - "path": null - }, - { - "display_name": "Fantasy", - "group_id": "14", - "path": null - }, - { - "display_name": "Family", - "group_id": "10751", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Animation", - "group_id": "16", - "path": null - }, - { - "display_name": "Science Fiction", - "group_id": "878", - "path": null - } - ] - }, - "matched_terms": [ - "titans" - ], - "value": "titans" - }, - { - "data": { - "groups": [ - { - "display_name": "Animation", - "group_id": "16", - "path": null - }, - { - "display_name": "Action", - "group_id": "28", - "path": null - }, - { - "display_name": "Science Fiction", - "group_id": "878", - "path": null - }, - { - "display_name": "Family", - "group_id": "10751", - "path": null - }, - { - "display_name": "Adventure", - "group_id": "12", - "path": null - }, - { - "display_name": "Documentary", - "group_id": "99", - "path": null - }, - { - "display_name": "Horror", - "group_id": "27", - "path": null - }, - { - "display_name": "Fantasy", - "group_id": "14", - "path": null - }, - { - "display_name": "Drama", - "group_id": "18", - "path": null - } - ] - }, - "matched_terms": [ - "titan" - ], - "value": "titan" - } - ] - } -} \ No newline at end of file diff --git a/library/src/test/resources/search_response_unexpected_data.json b/library/src/test/resources/search_response_unexpected_data.json deleted file mode 100755 index 7275c3f9..00000000 --- a/library/src/test/resources/search_response_unexpected_data.json +++ /dev/null @@ -1,599 +0,0 @@ -{ - "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" -}