diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt index ba5d1ed7f..598b88c7f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt @@ -8,7 +8,6 @@ import fr.free.nrw.commons.media.MediaClient import io.reactivex.Scheduler import io.reactivex.disposables.CompositeDisposable import timber.log.Timber -import java.util.* import javax.inject.Inject import javax.inject.Named @@ -52,11 +51,11 @@ class ContributionBoundaryCallback @Inject constructor( * Fetches contributions using the MediaWiki API */ fun fetchContributions() { - if (mediaClient.doesMediaListForUserHaveMorePages(sessionManager.userName).not()) { + if (mediaClient.doesMediaListForUserHaveMorePages(sessionManager.userName!!).not()) { return } compositeDisposable.add( - mediaClient.getMediaListForUser(sessionManager.userName) + mediaClient.getMediaListForUser(sessionManager.userName!!) .map { mediaList: List -> mediaList.map { Contribution(it, Contribution.STATE_COMPLETED) @@ -86,4 +85,4 @@ class ContributionBoundaryCallback @Inject constructor( } ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt index 641de6d6a..8572b42bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaDataSource.kt @@ -6,7 +6,7 @@ import fr.free.nrw.commons.explore.LiveDataConverter import fr.free.nrw.commons.explore.PageableDataSource import fr.free.nrw.commons.explore.depictions.LoadFunction import fr.free.nrw.commons.media.MediaClient -import fr.free.nrw.commons.media.MediaClient.NO_CAPTION +import fr.free.nrw.commons.media.MediaClient.Companion.NO_CAPTION import javax.inject.Inject class PageableMediaDataSource @Inject constructor( diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java deleted file mode 100644 index 846864979..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.java +++ /dev/null @@ -1,277 +0,0 @@ -package fr.free.nrw.commons.media; - - -import androidx.annotation.NonNull; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.utils.CommonsDateUtil; -import io.reactivex.Observable; -import io.reactivex.Single; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import javax.inject.Inject; -import javax.inject.Singleton; -import org.wikipedia.dataclient.mwapi.MwQueryResponse; -import org.wikipedia.wikidata.Entities; -import org.wikipedia.wikidata.Entities.Entity; -import org.wikipedia.wikidata.Entities.Label; -import timber.log.Timber; - -/** - * Media Client to handle custom calls to Commons MediaWiki APIs - */ -@Singleton -public class MediaClient { - - private final MediaInterface mediaInterface; - private final PageMediaInterface pageMediaInterface; - private final MediaDetailInterface mediaDetailInterface; - - //OkHttpJsonApiClient used JsonKvStore for this. I don't know why. - private Map> continuationStore; - private Map continuationExists; - public static final String NO_CAPTION = "No caption"; - private static final String NO_DEPICTION = "No depiction"; - - @Inject - public MediaClient(MediaInterface mediaInterface, - PageMediaInterface pageMediaInterface, - MediaDetailInterface mediaDetailInterface) { - this.mediaInterface = mediaInterface; - this.pageMediaInterface = pageMediaInterface; - this.mediaDetailInterface = mediaDetailInterface; - this.continuationStore = new HashMap<>(); - this.continuationExists = new HashMap<>(); - } - - /** - * Checks if a page exists on Commons - * The same method can be used to check for file or talk page - * - * @param title File:Test.jpg or Commons:Deletion_requests/File:Test1.jpeg - */ - public Single checkPageExistsUsingTitle(String title) { - return mediaInterface.checkPageExistsUsingTitle(title) - .map(mwQueryResponse -> mwQueryResponse - .query().firstPage().pageId() > 0) - .singleOrError(); - } - - /** - * Take the fileSha and returns whether a file with a matching SHA exists or not - * - * @param fileSha SHA of the file to be checked - */ - public Single checkFileExistsUsingSha(String fileSha) { - return mediaInterface.checkFileExistsUsingSha(fileSha) - .map(mwQueryResponse -> mwQueryResponse - .query().allImages().size() > 0) - .singleOrError(); - } - - /** - * This method takes the category as input and returns a list of Media objects filtered using image generator query - * It uses the generator query API to get the images searched using a query, 10 at a time. - * - * @param category the search category. Must start with "Category:" - * @return - */ - public Single> getMediaListFromCategory(String category) { - return responseToMediaList( - continuationStore.containsKey("category_" + category) ? - mediaInterface.getMediaListFromCategory(category, 10, continuationStore.get("category_" + category)) : //if true - mediaInterface.getMediaListFromCategory(category, 10, Collections.emptyMap()), - "category_" + category); //if false - - } - - /** - * This method takes the userName as input and returns a list of Media objects filtered using - * allimages query It uses the allimages query API to get the images contributed by the userName, - * 10 at a time. - * - * @param userName the username - * @return - */ - public Single> getMediaListForUser(String userName) { - Map continuation = - continuationStore.containsKey("user_" + userName) - ? continuationStore.get("user_" + userName) - : Collections.emptyMap(); - return responseToMediaList(mediaInterface - .getMediaListForUser(userName, 10, continuation), "user_" + userName); - } - - /** - * Check if media for user has reached the end of the list. - * @param userName - * @return - */ - public boolean doesMediaListForUserHaveMorePages(String userName) { - final String key = "user_" + userName; - if(continuationExists.containsKey(key)) { - return continuationExists.get(key); - } - return true; - } - - /** - * This method takes a keyword as input and returns a list of Media objects filtered using image generator query - * It uses the generator query API to get the images searched using a query, 10 at a time. - * - * @param keyword the search keyword - * @param limit - * @param offset - * @return - */ - public Single getMediaListFromSearch(String keyword, int limit, int offset) { - return mediaInterface.getMediaListFromSearch(keyword, limit, offset); - - } - - private Single> responseToMediaList(Observable response, String key) { - return response.flatMap(mwQueryResponse -> { - if (null == mwQueryResponse - || null == mwQueryResponse.query() - || null == mwQueryResponse.query().pages()) { - return Observable.empty(); - } - if(mwQueryResponse.continuation() != null) { - continuationStore.put(key, mwQueryResponse.continuation()); - continuationExists.put(key, true); - } else { - continuationExists.put(key, false); - } - return Observable.fromIterable(mwQueryResponse.query().pages()); - }) - .map(Media::from) - .collect(ArrayList::new, List::add); - } - - /** - * Fetches Media object from the imageInfo API - * - * @param titles the tiles to be searched for. Can be filename or template name - * @return - */ - public Single getMedia(String titles) { - return mediaInterface.getMedia(titles) - .flatMap(mwQueryResponse -> { - if (null == mwQueryResponse - || null == mwQueryResponse.query() - || null == mwQueryResponse.query().firstPage()) { - return Observable.empty(); - } - return Observable.just(mwQueryResponse.query().firstPage()); - }) - .map(Media::from) - .single(Media.EMPTY); - } - - /** - * The method returns the picture of the day - * - * @return Media object corresponding to the picture of the day - */ - @NonNull - public Single getPictureOfTheDay() { - String date = CommonsDateUtil.getIso8601DateFormatShort().format(new Date()); - Timber.d("Current date is %s", date); - String template = "Template:Potd/" + date; - return mediaInterface.getMediaWithGenerator(template) - .flatMap(mwQueryResponse -> { - if (null == mwQueryResponse - || null == mwQueryResponse.query() - || null == mwQueryResponse.query().firstPage()) { - return Observable.empty(); - } - return Observable.just(mwQueryResponse.query().firstPage()); - }) - .map(Media::from) - .single(Media.EMPTY); - } - - - @NonNull - public Single getPageHtml(String title){ - return mediaInterface.getPageHtml(title) - .filter(MwParseResponse::success) - .map(MwParseResponse::parse) - .map(MwParseResult::text) - .first(""); - } - - - /** - * @return caption for image using wikibaseIdentifier - */ - public Single getCaptionByWikibaseIdentifier(String wikibaseIdentifier) { - return mediaDetailInterface.getEntityForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier) - .map(mediaDetailResponse -> { - if (isSuccess(mediaDetailResponse)) { - for (Entity wikibaseItem : mediaDetailResponse.entities().values()) { - for (Label label : wikibaseItem.labels().values()) { - return label.value(); - } - } - } - return NO_CAPTION; - }) - .singleOrError(); - } - - public Single doesPageContainMedia(String title) { - return pageMediaInterface.getMediaList(title) - .map(pageMediaListResponse -> { - return pageMediaListResponse.getItems().size() > 0; - }).singleOrError(); - } - - private boolean isSuccess(Entities response) { - return response != null && response.getSuccess() == 1 && response.entities() != null; - } - - /** - * Fetches Structured data from API - * - * @param filename - * @return a map containing caption and depictions (empty string in the map if no caption/depictions) - */ - public Single getDepictions(String filename) { - return mediaDetailInterface.fetchEntitiesByFileName(Locale.getDefault().getLanguage(), filename) - .map(entities -> Depictions.from(entities, this)) - .singleOrError(); - } - - /** - * Gets labels for Depictions using Entity Id from MediaWikiAPI - * - * @param entityId EntityId (Ex: Q81566) of the depict entity - * @return label - */ - public Single getLabelForDepiction(String entityId, String language) { - return mediaDetailInterface.getEntity(entityId) - .map(entities -> { - if (isSuccess(entities)) { - for (Entity entity : entities.entities().values()) { - final Map languageToLabelMap = entity.labels(); - if (languageToLabelMap.containsKey(language)) { - return languageToLabelMap.get(language).value(); - } - for (Label label : languageToLabelMap.values()) { - return label.value(); - } - } - } - throw new RuntimeException("failed getEntities"); - }); - } - - public Single getEntities(String entityId) { - return mediaDetailInterface.getEntity(entityId); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt new file mode 100644 index 000000000..c7c177364 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt @@ -0,0 +1,300 @@ +package fr.free.nrw.commons.media + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.media.Depictions.Companion.from +import fr.free.nrw.commons.utils.CommonsDateUtil +import io.reactivex.Observable +import io.reactivex.Single +import org.wikipedia.dataclient.mwapi.MwQueryPage +import org.wikipedia.dataclient.mwapi.MwQueryResponse +import org.wikipedia.wikidata.Entities +import timber.log.Timber +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.collections.ArrayList + +/** + * Media Client to handle custom calls to Commons MediaWiki APIs + */ +@Singleton +class MediaClient @Inject constructor( + private val mediaInterface: MediaInterface, + private val pageMediaInterface: PageMediaInterface, + private val mediaDetailInterface: MediaDetailInterface +) { + + //OkHttpJsonApiClient used JsonKvStore for this. I don't know why. + private val continuationStore: MutableMap?> + private val continuationExists: MutableMap + + /** + * Checks if a page exists on Commons + * The same method can be used to check for file or talk page + * + * @param title File:Test.jpg or Commons:Deletion_requests/File:Test1.jpeg + */ + fun checkPageExistsUsingTitle(title: String?): Single { + return mediaInterface.checkPageExistsUsingTitle(title) + .map { mwQueryResponse: MwQueryResponse -> + mwQueryResponse + .query()!!.firstPage()!!.pageId() > 0 + } + .singleOrError() + } + + /** + * Take the fileSha and returns whether a file with a matching SHA exists or not + * + * @param fileSha SHA of the file to be checked + */ + fun checkFileExistsUsingSha(fileSha: String?): Single { + return mediaInterface.checkFileExistsUsingSha(fileSha) + .map { mwQueryResponse: MwQueryResponse -> + mwQueryResponse + .query()!!.allImages().size > 0 + } + .singleOrError() + } + + /** + * This method takes the category as input and returns a list of Media objects filtered using image generator query + * It uses the generator query API to get the images searched using a query, 10 at a time. + * + * @param category the search category. Must start with "Category:" + * @return + */ + fun getMediaListFromCategory(category: String): Single> { + return responseToMediaList( + if (continuationStore.containsKey("category_$category")) mediaInterface.getMediaListFromCategory( + category, + 10, + continuationStore["category_$category"] + ) else //if true + mediaInterface.getMediaListFromCategory( + category, + 10, + emptyMap() + ), + "category_$category" + ) //if false + } + + /** + * This method takes the userName as input and returns a list of Media objects filtered using + * allimages query It uses the allimages query API to get the images contributed by the userName, + * 10 at a time. + * + * @param userName the username + * @return + */ + fun getMediaListForUser(userName: String): Single> { + val continuation = + if (continuationStore.containsKey("user_$userName")) continuationStore["user_$userName"] else emptyMap() + return responseToMediaList( + mediaInterface + .getMediaListForUser(userName, 10, continuation), "user_$userName" + ) + } + + /** + * Check if media for user has reached the end of the list. + * @param userName + * @return + */ + fun doesMediaListForUserHaveMorePages(userName: String): Boolean { + val key = "user_$userName" + return if (continuationExists.containsKey(key)) { + continuationExists[key]!! + } else true + } + + /** + * This method takes a keyword as input and returns a list of Media objects filtered using image generator query + * It uses the generator query API to get the images searched using a query, 10 at a time. + * + * @param keyword the search keyword + * @param limit + * @param offset + * @return + */ + fun getMediaListFromSearch( + keyword: String?, + limit: Int, + offset: Int + ): Single { + return mediaInterface.getMediaListFromSearch(keyword, limit, offset) + } + + private fun responseToMediaList( + response: Observable, + key: String + ): Single> { + return response.flatMap { mwQueryResponse: MwQueryResponse? -> + if (null == mwQueryResponse || null == mwQueryResponse.query() || null == mwQueryResponse.query()!! + .pages() + ) { + return@flatMap Observable.empty() + } + if (mwQueryResponse.continuation() != null) { + continuationStore[key] = mwQueryResponse.continuation() + continuationExists[key] = true + } else { + continuationExists[key] = false + } + Observable.fromIterable(mwQueryResponse.query()!!.pages()) + } + .map { page: MwQueryPage? -> Media.from(page) } + .collect( + { ArrayList() } + ) { obj: MutableList, e: Media -> + obj.add( + e + ) + }.map { it.toList() } + } + + /** + * Fetches Media object from the imageInfo API + * + * @param titles the tiles to be searched for. Can be filename or template name + * @return + */ + fun getMedia(titles: String?): Single { + return mediaInterface.getMedia(titles) + .flatMap { mwQueryResponse: MwQueryResponse? -> + if (null == mwQueryResponse || null == mwQueryResponse.query() || null == mwQueryResponse.query()!! + .firstPage() + ) { + return@flatMap Observable.empty() + } + Observable.just(mwQueryResponse.query()!!.firstPage()) + } + .map { page: MwQueryPage? -> Media.from(page) } + .single(Media.EMPTY) + } + + /** + * The method returns the picture of the day + * + * @return Media object corresponding to the picture of the day + */ + val pictureOfTheDay: Single + get() { + val date = + CommonsDateUtil.getIso8601DateFormatShort().format(Date()) + Timber.d("Current date is %s", date) + val template = "Template:Potd/$date" + return mediaInterface.getMediaWithGenerator(template) + .flatMap { mwQueryResponse: MwQueryResponse? -> + if (null == mwQueryResponse || null == mwQueryResponse.query() || null == mwQueryResponse.query()!! + .firstPage() + ) { + return@flatMap Observable.empty() + } + Observable.just(mwQueryResponse.query()!!.firstPage()) + } + .map { page: MwQueryPage? -> Media.from(page) } + .single(Media.EMPTY) + } + + fun getPageHtml(title: String?): Single { + return mediaInterface.getPageHtml(title) + .filter { obj: MwParseResponse -> obj.success() } + .map { obj: MwParseResponse -> obj.parse() } + .map { obj: MwParseResult? -> obj!!.text() } + .first("") + } + + /** + * @return caption for image using wikibaseIdentifier + */ + fun getCaptionByWikibaseIdentifier(wikibaseIdentifier: String?): Single { + return mediaDetailInterface.getEntityForImage( + Locale.getDefault().language, + wikibaseIdentifier + ) + .map { mediaDetailResponse: Entities -> + if (isSuccess(mediaDetailResponse)) { + for (wikibaseItem in mediaDetailResponse.entities().values) { + for (label in wikibaseItem.labels().values) { + return@map label.value() + } + } + } + NO_CAPTION + } + .singleOrError() + } + + fun doesPageContainMedia(title: String?): Single { + return pageMediaInterface.getMediaList(title) + .map { it.items.isNotEmpty() } + } + + private fun isSuccess(response: Entities?): Boolean { + return response != null && response.success == 1 && response.entities() != null + } + + /** + * Fetches Structured data from API + * + * @param filename + * @return a map containing caption and depictions (empty string in the map if no caption/depictions) + */ + fun getDepictions(filename: String?): Single { + return mediaDetailInterface.fetchEntitiesByFileName( + Locale.getDefault().language, filename + ) + .map { entities: Entities? -> + from( + entities!!, + this + ) + } + .singleOrError() + } + + /** + * Gets labels for Depictions using Entity Id from MediaWikiAPI + * + * @param entityId EntityId (Ex: Q81566) of the depict entity + * @return label + */ + fun getLabelForDepiction( + entityId: String?, + language: String + ): Single { + return mediaDetailInterface.getEntity(entityId) + .map { entities: Entities -> + if (isSuccess(entities)) { + for (entity in entities.entities().values) { + val languageToLabelMap = + entity.labels() + if (languageToLabelMap.containsKey(language)) { + return@map languageToLabelMap[language]!!.value() + } + for (label in languageToLabelMap.values) { + return@map label.value() + } + } + } + throw RuntimeException("failed getEntities") + } + } + + fun getEntities(entityId: String?): Single { + return mediaDetailInterface.getEntity(entityId) + } + + companion object { + const val NO_CAPTION = "No caption" + private const val NO_DEPICTION = "No depiction" + } + + init { + continuationStore = + HashMap() + continuationExists = HashMap() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/PageMediaInterface.kt b/app/src/main/java/fr/free/nrw/commons/media/PageMediaInterface.kt index 0be03b736..2e8927778 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/PageMediaInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/PageMediaInterface.kt @@ -1,7 +1,7 @@ package fr.free.nrw.commons.media import fr.free.nrw.commons.media.model.PageMediaListResponse -import io.reactivex.Observable +import io.reactivex.Single import retrofit2.http.GET import retrofit2.http.Path @@ -15,5 +15,5 @@ interface PageMediaInterface { * @param title the title of the page */ @GET("api/rest_v1/page/media-list/{title}") - fun getMediaList(@Path("title") title: String?): Observable? -} \ No newline at end of file + fun getMediaList(@Path("title") title: String?): Single +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionBoundaryCallbackTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionBoundaryCallbackTest.kt index b2b08d185..365c0ae7f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionBoundaryCallbackTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionBoundaryCallbackTest.kt @@ -1,27 +1,21 @@ package fr.free.nrw.commons.contributions -import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.nhaarman.mockitokotlin2.* import fr.free.nrw.commons.Media import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.media.MediaClient -import fr.free.nrw.commons.utils.NetworkUtilsTest -import fr.free.nrw.commons.utils.createMockDataSourceFactory import io.reactivex.Scheduler import io.reactivex.Single import io.reactivex.schedulers.Schedulers -import io.reactivex.schedulers.TestScheduler import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.ArgumentMatchers.* -import org.mockito.InjectMocks +import org.mockito.ArgumentMatchers.anyList +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations -import java.lang.RuntimeException -import java.util.* /** * The unit test class for ContributionBoundaryCallbackTest @@ -136,4 +130,4 @@ class ContributionBoundaryCallbackTest { verifyZeroInteractions(repository); verify(mediaClient).getMediaListForUser(anyString()); } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt index 1fe48fb8d..44fbc8303 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaClientTest.kt @@ -6,20 +6,18 @@ import fr.free.nrw.commons.media.model.PageMediaListItem import fr.free.nrw.commons.media.model.PageMediaListResponse import fr.free.nrw.commons.utils.CommonsDateUtil import io.reactivex.Observable +import io.reactivex.Single import junit.framework.Assert.* import org.junit.Before import org.junit.Test import org.mockito.* +import org.mockito.Mockito.* import org.wikipedia.dataclient.mwapi.ImageDetails import org.wikipedia.dataclient.mwapi.MwQueryPage import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.gallery.ImageInfo -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.* import java.util.* -import org.mockito.Captor -import org.mockito.Mockito.* class MediaClientTest { @@ -30,6 +28,9 @@ class MediaClientTest { @Mock internal var pageMediaInterface: PageMediaInterface? = null + @Mock + internal var mediaDetailInterface: MediaDetailInterface? = null + @InjectMocks var mediaClient: MediaClient? = null @@ -168,7 +169,7 @@ class MediaClientTest { `when`(mediaInterface!!.getMediaWithGenerator(filenameCaptor!!.capture())) .thenReturn(Observable.just(mockResponse)) - assertEquals("Test", mediaClient!!.getPictureOfTheDay().blockingGet().filename) + assertEquals("Test", mediaClient!!.pictureOfTheDay.blockingGet().filename) assertEquals(template, filenameCaptor.value); } @@ -260,7 +261,7 @@ class MediaClientTest { val mock = mock(PageMediaListResponse::class.java) whenever(mock.items).thenReturn(listOf(mock(PageMediaListItem::class.java))) `when`(pageMediaInterface!!.getMediaList(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mock)) + .thenReturn(Single.just(mock)) mediaClient!!.doesPageContainMedia("Test").test().assertValue(true) } @@ -270,7 +271,7 @@ class MediaClientTest { val mock = mock(PageMediaListResponse::class.java) whenever(mock.items).thenReturn(listOf()) `when`(pageMediaInterface!!.getMediaList(ArgumentMatchers.anyString())) - .thenReturn(Observable.just(mock)) + .thenReturn(Single.just(mock)) mediaClient!!.doesPageContainMedia("Test").test().assertValue(false) } @@ -285,4 +286,4 @@ class MediaClientTest { assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet()) } -} \ No newline at end of file +}