diff --git a/app/build.gradle b/app/build.gradle index 3e868e6d..908ff43b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,6 +14,9 @@ if (allFilesFromDir != null) { android { + testOptions { + unitTests.returnDefaultValues = true + } signingConfigs { prerelease { if (prerelaseStoreFile != null) { @@ -76,6 +79,8 @@ repositories { } dependencies { + testImplementation 'org.json:json:20180813' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.1' diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/KawaiifuProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/KawaiifuProvider.kt index 465cb8d7..e2ed00db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/KawaiifuProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/KawaiifuProvider.kt @@ -17,8 +17,7 @@ class KawaiifuProvider : MainAPI() { get() = false override val hasMainPage: Boolean get() = true - - + override val supportedTypes: Set get() = setOf(TvType.Anime, TvType.AnimeMovie, TvType.ONA) @@ -26,12 +25,12 @@ class KawaiifuProvider : MainAPI() { override fun getMainPage(): HomePageResponse { val items = ArrayList() val resp = get(mainUrl).text - println("RESP $resp") + val soup = Jsoup.parse(resp) items.add(HomePageList("Latest Updates", soup.select(".today-update .item").map { - val title = it.selectFirst("img").attr("alt") - AnimeSearchResponse( + val title = it.selectFirst("img").attr("alt") + AnimeSearchResponse( title, it.selectFirst("a").attr("href"), this.name, @@ -48,8 +47,8 @@ class KawaiifuProvider : MainAPI() { try { val title = section.selectFirst(".title").text() val anime = section.select(".list-film > .item").map { ani -> - val animTitle = ani.selectFirst("img").attr("alt") - AnimeSearchResponse( + val animTitle = ani.selectFirst("img").attr("alt") + AnimeSearchResponse( animTitle, ani.selectFirst("a").attr("href"), this.name, @@ -68,7 +67,7 @@ class KawaiifuProvider : MainAPI() { e.printStackTrace() } } - if(items.size <= 0) throw ErrorLoadingException() + if (items.size <= 0) throw ErrorLoadingException() return HomePageResponse(items) } @@ -151,7 +150,7 @@ class KawaiifuProvider : MainAPI() { val servers = soupa.select(".list-server").map { val serverName = it.selectFirst(".server-name").text() - val episodes = it.select(".list-ep > li > a").map { episode -> Pair(episode.attr("href"), episode.text()) } + val episodes = it.select(".list-ep > li > a").map { episode -> Pair(episode.attr("href"), episode.text()) } val episode = if (episodeNum == null) episodes[0] else episodes.mapNotNull { ep -> if ((if (ep.first.contains("ep=")) ep.first.split("ep=")[1].split("&")[0].toIntOrNull() else null) == episodeNum) { ep @@ -160,27 +159,31 @@ class KawaiifuProvider : MainAPI() { Pair(serverName, episode) }.map { if (it.second.first == data) { - val sources = soupa.select("video > source").map { source -> Pair(source.attr("src"), source.attr("data-quality")) } + val sources = soupa.select("video > source") + .map { source -> Pair(source.attr("src"), source.attr("data-quality")) } Triple(it.first, sources, it.second.second) } else { val html = get(it.second.first).text val soup = Jsoup.parse(html) - val sources = soup.select("video > source").map { source -> Pair(source.attr("src"), source.attr("data-quality")) } + val sources = soup.select("video > source") + .map { source -> Pair(source.attr("src"), source.attr("data-quality")) } Triple(it.first, sources, it.second.second) } } servers.forEach { it.second.forEach { source -> - callback(ExtractorLink( - "Kawaiifu", - "${it.first} - ${source.second}", - source.first, - "", - getQualityFromName(source.second), - source.first.contains(".m3u") - )) + callback( + ExtractorLink( + "Kawaiifu", + "${it.first} - ${source.second}", + source.first, + "", + getQualityFromName(source.second), + source.first.contains(".m3u") + ) + ) } } return true diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/WatchCartoonOnlineProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/WatchCartoonOnlineProvider.kt index d465120e..ed25a342 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/WatchCartoonOnlineProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/WatchCartoonOnlineProvider.kt @@ -24,6 +24,8 @@ class WatchCartoonOnlineProvider : MainAPI() { get() = setOf( TvType.Cartoon, TvType.Anime, + TvType.AnimeMovie, + TvType.TvSeries ) override fun search(query: String): List { @@ -109,7 +111,6 @@ class WatchCartoonOnlineProvider : MainAPI() { val response = get(url).text val document = Jsoup.parse(response) - return if (!isMovie) { val title = document.selectFirst("td.vsbaslik > h2").text() val poster = fixUrl(document.selectFirst("div#cat-img-desc > div > img").attr("src")) diff --git a/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt b/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt new file mode 100644 index 00000000..cc6b588b --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt @@ -0,0 +1,177 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.SubtitleHelper +import org.junit.Assert +import org.junit.Test + +class ProviderTests { + private fun getAllProviders(): List { + val allApis = APIHolder.apis + allApis.addAll(APIHolder.restrictedApis) + return allApis + } + + @Test + fun providers_exist() { + Assert.assertTrue(getAllProviders().isNotEmpty()) + } + + @Test + fun provider_correct_data() { + val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 } + Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty()) + for (api in getAllProviders()) { + Assert.assertTrue("Api does not contain a mainurl", api.mainUrl != "NONE") + Assert.assertTrue("Api does not contain a name", api.name != "NONE") + Assert.assertTrue("Api ${api.name} does not contain a valid language code", isoNames.contains(api.lang)) + Assert.assertTrue("Api ${api.name} does not contain any supported types", api.supportedTypes.isNotEmpty()) + } + } + + @Test + fun provider_correct_homepage() { + for (api in getAllProviders()) { + if (api.hasMainPage) { + try { + val homepage = api.getMainPage() + when { + homepage == null -> { + Assert.fail("Homepage provider ${api.name} did not correctly load homepage!") + } + homepage.items.isEmpty() -> { + Assert.fail("Homepage provider ${api.name} does not contain any items!") + } + homepage.items.any { it.list.isEmpty() } -> { + Assert.fail("Homepage provider ${api.name} does not have any items on result!") + } + } + } catch (e: Exception) { + if (e.cause is NotImplementedError) { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") + } + logError(e) + } + } + } + } + + private fun loadLinks(api: MainAPI, url: String?): Boolean { + Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) + if (url == null) return true + var linksLoaded = 0 + try { + val success = api.loadLinks(url, false, {}) { link -> + Assert.assertTrue( + "Api ${api.name} returns link with invalid Quality", + Qualities.values().map { it.value }.contains(link.quality) + ) + Assert.assertTrue("Api ${api.name} returns link with invalid url", link.url.length > 4) + linksLoaded++ + } + if (success) { + return linksLoaded > 0 + } + Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success) + } catch (e: Exception) { + if (e.cause is NotImplementedError) { + Assert.fail("Provider has not implemented .loadLinks") + } + logError(e) + } + return true + } + + @Test + fun provider_correct() { + val searchQueries = listOf("over", "iron", "guy") + val providers = getAllProviders() + for ((index, api) in providers.withIndex()) { + try { + println("Trying $api (${index + 1}/${providers.size})") + var correctResponses = 0 + var searchResult: List? = null + for (query in searchQueries) { + val response = try { + api.search(query) + } catch (e: Exception) { + if (e.cause is NotImplementedError) { + Assert.fail("Provider has not implemented .search") + } + logError(e) + null + } + if (!response.isNullOrEmpty()) { + correctResponses++ + if (searchResult == null) { + searchResult = response + } + } + } + + if (correctResponses == 0 || searchResult == null) { + println("Api ${api.name} did not return any valid search responses") + continue + } + + try { + var validResults = false + for (result in searchResult) { + Assert.assertEquals("Invalid apiName on response on ${api.name}", result.apiName, api.name) + val load = api.load(result.url) ?: continue + Assert.assertEquals("Invalid apiName on load on ${api.name}", load.apiName, result.apiName) + Assert.assertTrue( + "Api ${api.name} on load does not contain any of the supportedTypes", + api.supportedTypes.contains(load.type) + ) + when (load) { + is AnimeLoadResponse -> { + val gotNoEpisodes = + load.dubEpisodes.isNullOrEmpty() && load.subEpisodes.isNullOrEmpty() + if (gotNoEpisodes) { + println("Api ${api.name} got no episodes on ${load.url}") + continue + } + + val url = (load.dubEpisodes ?: load.subEpisodes)?.first()?.url + validResults = loadLinks(api, url) + if (!validResults) continue + } + is MovieLoadResponse -> { + val gotNoEpisodes = load.dataUrl.isBlank() + if (gotNoEpisodes) { + println("Api ${api.name} got no movie on ${load.url}") + continue + } + + validResults = loadLinks(api, load.dataUrl) + if (!validResults) continue + } + is TvSeriesLoadResponse -> { + val gotNoEpisodes = load.episodes.isEmpty() + if (gotNoEpisodes) { + println("Api ${api.name} got no episodes on ${load.url}") + continue + } + + validResults = loadLinks(api, load.episodes.first().data) + if (!validResults) continue + } + } + break + } + + Assert.assertTrue("Api ${api.name} did not load on any}", validResults) + } catch (e: Exception) { + if (e.cause is NotImplementedError) { + Assert.fail("Provider has not implemented .load") + } + logError(e) + } + } catch (e: Exception) { + logError(e) + } + } + } +} \ No newline at end of file