diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepository.kt index cdfcb1589a03..be35202d1880 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepository.kt @@ -1,66 +1,87 @@ package org.wordpress.android.ui.domains.management.newdomainsearch.domainsfetcher -import org.wordpress.android.Constants -import org.wordpress.android.fluxc.generated.SiteActionBuilder -import org.wordpress.android.fluxc.model.products.Product -import org.wordpress.android.fluxc.store.ProductsStore -import org.wordpress.android.fluxc.store.SiteStore -import org.wordpress.android.fluxc.store.SiteStore.OnSuggestedDomains +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.networking.restapi.WpComApiClientProvider +import rs.wordpress.api.kotlin.WpComApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.DomainSuggestion +import uniffi.wp_api.DomainSuggestionsParams +import uniffi.wp_api.Product +import uniffi.wp_api.ProductTypeFilter +import uniffi.wp_api.ProductsParams import javax.inject.Inject -private const val SUGGESTIONS_REQUEST_COUNT = 20 +private const val SUGGESTIONS_REQUEST_COUNT = 20u class NewDomainsSearchRepository @Inject constructor( - private val productsStore: ProductsStore, - private val suggestedDomainsFetcher: SuggestedDomainsFetcher + private val wpComApiClientProvider: WpComApiClientProvider, + private val accountStore: AccountStore, ) { - var products: List? = null + private var wpComApiClient: WpComApiClient? = null + private var products: List? = null + + @Synchronized + private fun getOrCreateClient(): WpComApiClient { + val token = requireNotNull(accountStore.accessToken) { + "WP.com access token is required" + } + return wpComApiClient + ?: wpComApiClientProvider.getWpComApiClient(token) + .also { wpComApiClient = it } + } suspend fun searchForDomains(query: String): DomainsResult { if (products == null) fetchProducts() - return SiteActionBuilder.newSuggestDomainsAction( - SiteStore.SuggestDomainsPayload( - query = query, - onlyWordpressCom = false, - includeWordpressCom = false, - includeDotBlogSubdomain = false, - quantity = SUGGESTIONS_REQUEST_COUNT - ) - ).let { action -> - suggestedDomainsFetcher.fetch(action) - }.let { event -> - onDomainSuggestionsFetched(query, event) + + val params = DomainSuggestionsParams( + query = query, + quantity = SUGGESTIONS_REQUEST_COUNT, + onlyWordpressdotcom = false, // checkstyle ignore + includeWordpressdotcom = false, // checkstyle ignore + includeDotblogsubdomain = false, + ) + + return when ( + val result = getOrCreateClient() + .request { it.domains().suggestions(params).data } + ) { + is WpRequestResult.Success -> { + val suggestions = result.response + .filterIsInstance() + .sortedByDescending { it.v1.relevance } + .map { paid -> + val product = products?.firstOrNull { + it.productId == paid.v1.productId + } + ProposedDomain( + productId = paid.v1.productId.toInt(), + domain = paid.v1.domainName, + price = paid.v1.cost, + salePrice = product?.combinedSaleCostDisplay, + supportsPrivacy = paid.v1.supportsPrivacy, + ) + } + DomainsResult.Success(suggestions) + } + else -> DomainsResult.Error } } private suspend fun fetchProducts() { - val result = productsStore.fetchProducts(Constants.TYPE_DOMAINS_PRODUCT) - if (!result.isError) result.products?.let { products = it } - } - - private fun onDomainSuggestionsFetched(query: String, event: OnSuggestedDomains): DomainsResult { - return if (query == event.query && !event.isError) { - val suggestions = event.suggestions - .filter { !it.is_free } - .sortedByDescending { it.relevance } - .map { domain -> - val product = products?.firstOrNull { product -> product.productId == domain.product_id } - ProposedDomain( - productId = domain.product_id, - domain = domain.domain_name, - price = domain.cost, - salePrice = product?.combinedSaleCostDisplay, - supportsPrivacy = domain.supports_privacy - ) - } - DomainsResult.Success(suggestions) - } else { - DomainsResult.Error + val params = ProductsParams( + productType = ProductTypeFilter.Domains + ) + val result = getOrCreateClient() + .request { it.products().list(params).data } + if (result is WpRequestResult.Success) { + products = result.response.values.toList() } } sealed interface DomainsResult { - data class Success(val proposedDomains: List) : DomainsResult - object Error : DomainsResult + data class Success( + val proposedDomains: List + ) : DomainsResult + data object Error : DomainsResult } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/SuggestedDomainsFetcher.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/SuggestedDomainsFetcher.kt deleted file mode 100644 index 5cfa3e229907..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/SuggestedDomainsFetcher.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.wordpress.android.ui.domains.management.newdomainsearch.domainsfetcher - -import org.wordpress.android.fluxc.Dispatcher -import org.wordpress.android.fluxc.annotations.action.Action -import org.wordpress.android.fluxc.store.SiteStore.OnSuggestedDomains -import org.wordpress.android.fluxc.store.SiteStore.SuggestDomainsPayload -import org.wordpress.android.util.dispatchAndAwait -import javax.inject.Inject - -class SuggestedDomainsFetcher @Inject constructor( - private val dispatcher: Dispatcher -) { - suspend fun fetch(action: Action): OnSuggestedDomains = dispatcher.dispatchAndAwait(action) -} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepositoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepositoryTest.kt index f2ecdee912b8..daaa3938ae6d 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepositoryTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/newdomainsearch/domainsfetcher/NewDomainsSearchRepositoryTest.kt @@ -2,306 +2,314 @@ package org.wordpress.android.ui.domains.management.newdomainsearch.domainsfetch import kotlinx.coroutines.ExperimentalCoroutinesApi import org.assertj.core.api.Assertions.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest -import org.wordpress.android.Constants -import org.wordpress.android.fluxc.model.products.Product -import org.wordpress.android.fluxc.network.rest.wpcom.site.DomainSuggestionResponse -import org.wordpress.android.fluxc.store.ProductsStore -import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.networking.restapi.WpComApiClientProvider +import rs.wordpress.api.kotlin.WpComApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.DomainSuggestion +import uniffi.wp_api.PaidDomainSuggestion +import uniffi.wp_api.Product +import uniffi.wp_api.RequestMethod @Suppress("MaxLineLength") @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class NewDomainsSearchRepositoryTest : BaseUnitTest() { @Mock - private lateinit var productsStore: ProductsStore + private lateinit var wpComApiClientProvider: WpComApiClientProvider @Mock - private lateinit var suggestedDomainsFetcher: SuggestedDomainsFetcher + private lateinit var accountStore: AccountStore + + @Mock + private lateinit var wpComApiClient: WpComApiClient - @InjectMocks private lateinit var repository: NewDomainsSearchRepository + @Before + fun setUp() { + whenever(accountStore.accessToken).thenReturn("test-token") + whenever(wpComApiClientProvider.getWpComApiClient("test-token")) + .thenReturn(wpComApiClient) + repository = NewDomainsSearchRepository( + wpComApiClientProvider, + accountStore + ) + } + @Test - fun `GIVEN successfully fetched products with sale price available and suggestions WHEN searchForDomains THEN return DomainsResult Success with suggestions`() = + fun `GIVEN product with sale WHEN searchForDomains THEN sale price comes from product`() = test { - mockProductWithSaleResponse() - mockSuccessfulDomainFetchResponse() - - val result = repository.searchForDomains("query") - - assertThat(result).isEqualTo( - NewDomainsSearchRepository.DomainsResult.Success( - proposedDomains = listOf( - ProposedDomain( - productId = 0, - domain = "example.com", - price = "USD 50", - salePrice = "USD 10", - supportsPrivacy = true + mockProductsThenSuggestions( + products = mapOf( + "domain_reg" to testProduct( + productId = 6u, + combinedSaleCostDisplay = "$7.00" + ) + ), + suggestions = listOf( + DomainSuggestion.Paid( + paidSuggestion( + domainName = "example.com", + cost = "\$50.00", + productId = 6u, ) ) ) ) - } - - @Test - fun `GIVEN product with sale does not appear in domains fetch response WHEN searchForDomains THEN return DomainsResult Success with suggestions with no sale`() = - test { - mockProductWithSaleResponse(productId = 1) - mockSuccessfulDomainFetchResponse() val result = repository.searchForDomains("query") - assertThat(result).isEqualTo( - NewDomainsSearchRepository.DomainsResult.Success( - proposedDomains = listOf( - ProposedDomain( - productId = 0, - domain = "example.com", - price = "USD 50", - salePrice = null, - supportsPrivacy = true - ) - ) - ) - ) + val success = result as NewDomainsSearchRepository.DomainsResult.Success + assertThat(success.proposedDomains).hasSize(1) + assertThat(success.proposedDomains[0].domain).isEqualTo("example.com") + assertThat(success.proposedDomains[0].price).isEqualTo("\$50.00") + assertThat(success.proposedDomains[0].salePrice).isEqualTo("$7.00") } - @Suppress("LongMethod") @Test - fun `GIVEN few domains with different relevance WHEN searchForDomains THEN sort domains by descending by relevance`() = + fun `GIVEN product without sale WHEN searchForDomains THEN sale price is null`() = test { - mockFetchProductsError() - whenever(suggestedDomainsFetcher.fetch(any())).thenReturn( - SiteStore.OnSuggestedDomains( - query = "query", - suggestions = listOf( - DomainSuggestionResponse().apply { - product_id = 0 - domain_name = "first.com" - is_free = false - relevance = 1f - cost = "USD 30" - supports_privacy = true - }, - DomainSuggestionResponse().apply { - product_id = 1 - domain_name = "second.com" - is_free = false - relevance = 2f - cost = "USD 40" - supports_privacy = true - }, - DomainSuggestionResponse().apply { - product_id = 2 - domain_name = "third.com" - is_free = false - relevance = 0f - cost = "USD 50" - supports_privacy = true - }, + mockProductsThenSuggestions( + products = mapOf( + "domain_reg" to testProduct( + productId = 6u, + combinedSaleCostDisplay = null + ) + ), + suggestions = listOf( + DomainSuggestion.Paid( + paidSuggestion( + domainName = "example.com", + cost = "\$50.00", + productId = 6u, + ) ) ) ) val result = repository.searchForDomains("query") - assertThat(result).isEqualTo( - NewDomainsSearchRepository.DomainsResult.Success( - proposedDomains = listOf( - ProposedDomain( - productId = 1, - domain = "second.com", - price = "USD 40", - salePrice = null, - supportsPrivacy = true - ), - ProposedDomain( - productId = 0, - domain = "first.com", - price = "USD 30", - salePrice = null, - supportsPrivacy = true - ), - ProposedDomain(productId = 2, - domain = "third.com", - price = "USD 50", - salePrice = null, - supportsPrivacy = true - ), - ) - ) - ) + val success = result as NewDomainsSearchRepository.DomainsResult.Success + assertThat(success.proposedDomains[0].salePrice).isNull() } @Test - fun `GIVEN few domains with a free domain WHEN searchForDomains THEN filter out free domains`() = + fun `GIVEN no matching product WHEN searchForDomains THEN sale price is null`() = test { - mockFetchProductsError() - whenever(suggestedDomainsFetcher.fetch(any())).thenReturn( - SiteStore.OnSuggestedDomains( - query = "query", - suggestions = listOf( - DomainSuggestionResponse().apply { - product_id = 0 - domain_name = "first.com" - is_free = false - relevance = 1f - cost = "USD 30" - supports_privacy = true - }, - DomainSuggestionResponse().apply { - product_id = 1 - domain_name = "second.com" - is_free = false - relevance = 2f - cost = "USD 40" - supports_privacy = true - }, - DomainSuggestionResponse().apply { - product_id = 2 - domain_name = "third.com" - is_free = true - relevance = 0f - cost = "USD 50" - supports_privacy = true - }, + mockProductsThenSuggestions( + products = mapOf( + "other_product" to testProduct( + productId = 99u, + combinedSaleCostDisplay = "$5.00" + ) + ), + suggestions = listOf( + DomainSuggestion.Paid( + paidSuggestion( + domainName = "example.com", + productId = 6u, + ) ) ) ) val result = repository.searchForDomains("query") - assertThat(result).isEqualTo( - NewDomainsSearchRepository.DomainsResult.Success( - proposedDomains = listOf( - ProposedDomain( - productId = 1, - domain = "second.com", - price = "USD 40", - salePrice = null, - supportsPrivacy = true - ), - ProposedDomain( - productId = 0, - domain = "first.com", - price = "USD 30", - salePrice = null, - supportsPrivacy = true - ), - ) - ) - ) + val success = result as NewDomainsSearchRepository.DomainsResult.Success + assertThat(success.proposedDomains[0].salePrice).isNull() } @Test - fun `GIVEN product fetch with error and successful suggestions response WHEN searchForDomains THEN return DomainsResult Success with suggestions with no sale`() = + fun `GIVEN domains with different relevance WHEN searchForDomains THEN sort descending by relevance`() = test { - mockFetchProductsError() - mockSuccessfulDomainFetchResponse() + mockProductsThenSuggestions( + suggestions = listOf( + DomainSuggestion.Paid( + paidSuggestion(domainName = "first.com", relevance = 1.0) + ), + DomainSuggestion.Paid( + paidSuggestion(domainName = "second.com", relevance = 2.0) + ), + DomainSuggestion.Paid( + paidSuggestion(domainName = "third.com", relevance = 0.0) + ), + ) + ) val result = repository.searchForDomains("query") - assertThat(result).isEqualTo( - NewDomainsSearchRepository.DomainsResult.Success( - proposedDomains = listOf( - ProposedDomain( - productId = 0, - domain = "example.com", - price = "USD 50", - salePrice = null, - supportsPrivacy = true - ) - ) - ) - ) + val success = result as NewDomainsSearchRepository.DomainsResult.Success + assertThat(success.proposedDomains.map { it.domain }) + .containsExactly("second.com", "first.com", "third.com") } @Test - fun `GIVEN product fetch with error and suggestions with error WHEN searchForDomains THEN return DomainsResult Error`() = + fun `GIVEN mix of free and paid domains WHEN searchForDomains THEN filter out free domains`() = test { - mockFetchProductsError() - mockDomainsFetchError() + mockProductsThenSuggestions( + suggestions = listOf( + DomainSuggestion.Paid( + paidSuggestion(domainName = "paid.com", relevance = 1.0) + ), + DomainSuggestion.Free( + uniffi.wp_api.FreeDomainSuggestion( + domainName = "free.wordpress.com", + cost = "Free", + isFree = true + ) + ), + ) + ) val result = repository.searchForDomains("query") - assertThat(result).isEqualTo(NewDomainsSearchRepository.DomainsResult.Error) + val success = result as NewDomainsSearchRepository.DomainsResult.Success + assertThat(success.proposedDomains).hasSize(1) + assertThat(success.proposedDomains[0].domain).isEqualTo("paid.com") } @Test - fun `GIVEN two search calls WHEN searchForDomains THEN fetch products only once`() = + fun `GIVEN products fetch fails WHEN searchForDomains THEN return Success with null sale prices`() = test { - mockProductWithSaleResponse() - mockSuccessfulDomainFetchResponse("incorrect query") + mockProductsErrorThenSuggestions( + suggestions = listOf( + DomainSuggestion.Paid( + paidSuggestion( + domainName = "example.com", + cost = "\$50.00", + productId = 6u, + ) + ) + ) + ) val result = repository.searchForDomains("query") - assertThat(result).isEqualTo(NewDomainsSearchRepository.DomainsResult.Error) + val success = + result as NewDomainsSearchRepository.DomainsResult.Success + assertThat(success.proposedDomains).hasSize(1) + assertThat(success.proposedDomains[0].domain) + .isEqualTo("example.com") + assertThat(success.proposedDomains[0].price) + .isEqualTo("\$50.00") + assertThat(success.proposedDomains[0].salePrice).isNull() } @Test - fun `GIVEN event query is not equal to onDomainSuggestionFetched query argument WHEN searchForDomains THEN return error`() = + fun `GIVEN API error WHEN searchForDomains THEN return Error`() = test { - mockProductWithSaleResponse() - mockSuccessfulDomainFetchResponse() + mockProductsThenError() - repository.searchForDomains("query") - repository.searchForDomains("query") + val result = repository.searchForDomains("query") - verify(productsStore, times(1)).fetchProducts(Constants.TYPE_DOMAINS_PRODUCT) + assertThat(result) + .isEqualTo(NewDomainsSearchRepository.DomainsResult.Error) } - private suspend fun mockProductWithSaleResponse(productId: Int = 0) { - whenever(productsStore.fetchProducts(Constants.TYPE_DOMAINS_PRODUCT)).thenReturn( - ProductsStore.OnProductsFetched( - products = listOf(Product(productId = productId, combinedSaleCostDisplay = "USD 10",)) + @Suppress("UNCHECKED_CAST") + private suspend fun mockProductsThenSuggestions( + products: Map = emptyMap(), + suggestions: List, + ) { + whenever(wpComApiClient.request(any())) + .thenReturn( + WpRequestResult.Success(products) as WpRequestResult, + WpRequestResult.Success(suggestions) as WpRequestResult, ) - ) } - private suspend fun mockFetchProductsError() { - whenever(productsStore.fetchProducts(Constants.TYPE_DOMAINS_PRODUCT)).thenReturn( - ProductsStore.OnProductsFetched(error = mock()) - ) - } - - private suspend fun mockSuccessfulDomainFetchResponse( - query: String = "query", - productId: Int = 0, - domainName: String = "example.com", - isFree: Boolean = false, + @Suppress("UNCHECKED_CAST") + private suspend fun mockProductsErrorThenSuggestions( + suggestions: List, ) { - whenever(suggestedDomainsFetcher.fetch(any())).thenReturn( - SiteStore.OnSuggestedDomains( - query = query, - suggestions = listOf( - DomainSuggestionResponse().apply { - product_id = productId - domain_name = domainName - is_free = isFree - relevance = 0f - cost = "USD 50" - supports_privacy = true - } - ) + whenever(wpComApiClient.request(any())) + .thenReturn( + WpRequestResult.UnknownError( + 500.toUInt(), + "Internal Server Error", + "", + RequestMethod.GET + ), + WpRequestResult.Success(suggestions) + as WpRequestResult, ) - ) } - private suspend fun mockDomainsFetchError() { - whenever(suggestedDomainsFetcher.fetch(any())).thenReturn( - SiteStore.OnSuggestedDomains("query", emptyList()).apply { error = mock() } - ) + @Suppress("UNCHECKED_CAST") + private suspend fun mockProductsThenError() { + whenever(wpComApiClient.request(any())) + .thenReturn( + WpRequestResult.Success(emptyMap()) as WpRequestResult, + WpRequestResult.UnknownError( + 500.toUInt(), + "Internal Server Error", + "", + RequestMethod.GET + ), + ) } + + private fun paidSuggestion( + domainName: String = "example.com", + relevance: Double = 0.0, + cost: String = "\$18.00", + productId: ULong = 6u, + supportsPrivacy: Boolean = true, + ) = PaidDomainSuggestion( + domainName = domainName, + relevance = relevance, + supportsPrivacy = supportsPrivacy, + vendor = "donuts", + matchReasons = listOf("tld-common"), + maxRegYears = 10u, + multiYearRegAllowed = true, + productId = productId, + productSlug = "domain_reg", + cost = cost, + renewCost = cost, + renewRawPrice = 1800L, + rawPrice = 1800L, + currencyCode = "USD", + saleCost = null, + hstsRequired = null, + policyNotices = emptyList(), + ) + + private fun testProduct( + productId: ULong = 6u, + combinedSaleCostDisplay: String? = null, + ) = Product( + productId = productId, + productName = "Domain Registration", + productSlug = "domain_reg", + description = "Register a domain", + productType = "domains", + available = true, + billingProductSlug = "domain_reg", + isDomainRegistration = true, + costDisplay = "$18.00", + combinedCostDisplay = "$18", + cost = 1800L, + costSmallestUnit = 1800u, + currencyCode = "USD", + productTerm = uniffi.wp_api.ProductTerm.Year, + productTermLocalized = "year", + priceTierSlug = "", + priceTierList = emptyList(), + domainInfo = null, + costPerMonthDisplay = null, + saleCost = null, + combinedSaleCostDisplay = combinedSaleCostDisplay, + saleCoupon = null, + introductoryOffer = null, + ) }