diff --git a/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImpl.kt b/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImpl.kt index 3801f09b59b..1d3b2da4c5c 100644 --- a/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImpl.kt +++ b/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImpl.kt @@ -16,7 +16,7 @@ class ImprovedSearchManagerImpl @Inject constructor( ) : ImprovedSearchManager { override suspend fun autoCompleteSearch(term: String): List { val response = autoCompleteSearchService.autoCompleteSearch(query = term, termsLimit = null, podcastsLimit = null) - return response.results.map { + return response.results.mapNotNull { when (it) { is AutoCompleteResult.TermResult -> SearchAutoCompleteItem.Term(term = it.value) @@ -26,6 +26,8 @@ class ImprovedSearchManagerImpl @Inject constructor( author = it.value.author.orEmpty(), isExplicit = it.value.explicit == true, ) + + AutoCompleteResult.Unknown -> null } } } @@ -53,6 +55,8 @@ class ImprovedSearchManagerImpl @Inject constructor( publishedDate = it.publishedDate, duration = it.duration.seconds, ) + + CombinedResult.Unknown -> null } } } diff --git a/modules/services/repositories/src/test/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImplTest.kt b/modules/services/repositories/src/test/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImplTest.kt index d63863fe4a9..578784072f1 100644 --- a/modules/services/repositories/src/test/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImplTest.kt +++ b/modules/services/repositories/src/test/java/au/com/shiftyjelly/pocketcasts/repositories/search/ImprovedSearchManagerImplTest.kt @@ -1,16 +1,21 @@ package au.com.shiftyjelly.pocketcasts.repositories.search import au.com.shiftyjelly.pocketcasts.models.to.ImprovedSearchResultItem +import au.com.shiftyjelly.pocketcasts.models.to.SearchAutoCompleteItem import au.com.shiftyjelly.pocketcasts.servers.podcast.PodcastCacheService +import au.com.shiftyjelly.pocketcasts.servers.search.AutoCompleteResponse +import au.com.shiftyjelly.pocketcasts.servers.search.AutoCompleteResult import au.com.shiftyjelly.pocketcasts.servers.search.AutoCompleteSearchService import au.com.shiftyjelly.pocketcasts.servers.search.CombinedResult import au.com.shiftyjelly.pocketcasts.servers.search.CombinedSearchResponse +import au.com.shiftyjelly.pocketcasts.servers.search.PodcastResultValue import java.util.Date import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -66,4 +71,47 @@ class ImprovedSearchManagerImplTest { val episode = results.filterIsInstance().single() assertEquals("Business Daily", episode.podcastTitle) } + + @Test + fun `combined search drops unknown result types`() = runTest { + whenever(combinedSearchService.combinedSearch(any())) doReturn CombinedSearchResponse( + results = listOf( + CombinedResult.PodcastResult( + uuid = "podcast-uuid", + title = "Big Sugar", + author = "Weekday Fun Productions", + slug = "big-sugar", + explicit = false, + ), + CombinedResult.Unknown, + ), + ) + + val results = manager.combinedSearch("big sugar") + + assertEquals(listOf("podcast-uuid"), results.map { it.uuid }) + } + + @Test + fun `autocomplete search drops unknown result types`() = runTest { + whenever(autoCompleteSearchService.autoCompleteSearch(any(), anyOrNull(), anyOrNull())) doReturn AutoCompleteResponse( + results = listOf( + AutoCompleteResult.TermResult(value = "big sugar"), + AutoCompleteResult.PodcastResult( + value = PodcastResultValue(uuid = "podcast-uuid", title = "Big Sugar"), + ), + AutoCompleteResult.Unknown, + ), + ) + + val results = manager.autoCompleteSearch("big sugar") + + assertEquals( + listOf( + SearchAutoCompleteItem.Term(term = "big sugar"), + SearchAutoCompleteItem.Podcast(uuid = "podcast-uuid", title = "Big Sugar", author = "", isExplicit = false), + ), + results, + ) + } } diff --git a/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/AutoCompleteResponse.kt b/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/AutoCompleteResponse.kt index d5513038610..e4380a2e8de 100644 --- a/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/AutoCompleteResponse.kt +++ b/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/AutoCompleteResponse.kt @@ -19,10 +19,13 @@ sealed class AutoCompleteResult { val value: PodcastResultValue, ) : AutoCompleteResult() + data object Unknown : AutoCompleteResult() + companion object { val jsonAdapter = PolymorphicJsonAdapterFactory.of(AutoCompleteResult::class.java, "type") .withSubtype(TermResult::class.java, "term") .withSubtype(PodcastResult::class.java, "podcast") + .withDefaultValue(Unknown) } } diff --git a/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/CombinedSearchResponse.kt b/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/CombinedSearchResponse.kt index b068dc5e44b..580607e5c06 100644 --- a/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/CombinedSearchResponse.kt +++ b/modules/services/servers/src/main/java/au/com/shiftyjelly/pocketcasts/servers/search/CombinedSearchResponse.kt @@ -16,7 +16,7 @@ sealed interface CombinedResult { val uuid: String, val title: String? = null, val author: String? = "", - val slug: String, + val slug: String = "", val explicit: Boolean? = null, ) : CombinedResult @@ -33,12 +33,15 @@ sealed interface CombinedResult { @Json(name = "podcast_title") val podcastTitle: String, @Json(name = "podcast_slug") - val podcastSlug: String, + val podcastSlug: String = "", ) : CombinedResult + data object Unknown : CombinedResult + companion object { val jsonAdapter = PolymorphicJsonAdapterFactory.of(CombinedResult::class.java, "type") .withSubtype(PodcastResult::class.java, "podcast") .withSubtype(EpisodeResult::class.java, "episode") + .withDefaultValue(Unknown) } } diff --git a/modules/services/servers/src/test/kotlin/au/com/shiftyjelly/pocketcasts/servers/search/AutoCompleteResponseTest.kt b/modules/services/servers/src/test/kotlin/au/com/shiftyjelly/pocketcasts/servers/search/AutoCompleteResponseTest.kt new file mode 100644 index 00000000000..10becf46243 --- /dev/null +++ b/modules/services/servers/src/test/kotlin/au/com/shiftyjelly/pocketcasts/servers/search/AutoCompleteResponseTest.kt @@ -0,0 +1,28 @@ +package au.com.shiftyjelly.pocketcasts.servers.search + +import com.squareup.moshi.Moshi +import org.junit.Assert.assertEquals +import org.junit.Test + +class AutoCompleteResponseTest { + private val adapter = Moshi.Builder() + .add(AutoCompleteResult.jsonAdapter) + .build() + .adapter(AutoCompleteResponse::class.java) + + @Test + fun `unknown result types map to Unknown without failing the whole response`() { + val response = adapter.fromJson( + """ + {"results":[ + {"value":"freakonomics","type":"term"}, + {"value":{"uuid":"p1","title":"Freakonomics Radio"},"type":"podcast"}, + {"value":"anything","type":"network"} + ]} + """.trimIndent(), + ) + + val types = response?.results?.map { it::class.simpleName } + assertEquals(listOf("TermResult", "PodcastResult", "Unknown"), types) + } +} diff --git a/modules/services/servers/src/test/kotlin/au/com/shiftyjelly/pocketcasts/servers/search/CombinedSearchResponseTest.kt b/modules/services/servers/src/test/kotlin/au/com/shiftyjelly/pocketcasts/servers/search/CombinedSearchResponseTest.kt new file mode 100644 index 00000000000..e1ad44e8f49 --- /dev/null +++ b/modules/services/servers/src/test/kotlin/au/com/shiftyjelly/pocketcasts/servers/search/CombinedSearchResponseTest.kt @@ -0,0 +1,47 @@ +package au.com.shiftyjelly.pocketcasts.servers.search + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter +import java.util.Date +import org.junit.Assert.assertEquals +import org.junit.Test + +class CombinedSearchResponseTest { + private val adapter = Moshi.Builder() + .add(Date::class.java, Rfc3339DateJsonAdapter().nullSafe()) + .add(CombinedResult.jsonAdapter) + .build() + .adapter(CombinedSearchResponse::class.java) + + @Test + fun `unknown result types map to Unknown without failing the whole response`() { + val response = adapter.fromJson( + """ + {"results":[ + {"uuid":"p1","title":"Freakonomics Radio","slug":"freakonomics-radio","type":"podcast"}, + {"uuid":"n1","title":"Some Network","type":"network"}, + {"uuid":"p2","title":"Another Show","slug":"another-show","type":"podcast"} + ]} + """.trimIndent(), + ) + + val types = response?.results?.map { it::class.simpleName } + assertEquals(listOf("PodcastResult", "Unknown", "PodcastResult"), types) + } + + @Test + fun `registered podcast and episode subtypes decode alongside the fallback`() { + val response = adapter.fromJson( + """ + {"results":[ + {"uuid":"p1","title":"Freakonomics Radio","slug":"freakonomics-radio","type":"podcast"}, + {"uuid":"e1","title":"Ep 1","url":"https://example.com/1.mp3","published_date":"2024-01-02T03:04:05Z","podcast_uuid":"p1","podcast_title":"Freakonomics Radio","podcast_slug":"freakonomics-radio","type":"episode"}, + {"uuid":"n1","title":"Some Network","type":"network"} + ]} + """.trimIndent(), + ) + + val types = response?.results?.map { it::class.simpleName } + assertEquals(listOf("PodcastResult", "EpisodeResult", "Unknown"), types) + } +}