Improve remote repository tests
This commit is contained in:
@@ -102,6 +102,8 @@ dependencies {
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'com.google.truth:truth:1.1.3'
|
||||
testImplementation 'org.json:json:20210307'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1'
|
||||
testImplementation 'io.insert-koin:koin-test-junit4:3.1.2'
|
||||
}
|
||||
@@ -26,11 +26,8 @@ class MangareadRepository(
|
||||
sortOrder: SortOrder?,
|
||||
tag: MangaTag?
|
||||
): List<Manga> {
|
||||
if (offset % PAGE_SIZE != 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val payload = createRequestTemplate()
|
||||
payload["page"] = (offset / PAGE_SIZE).toString()
|
||||
payload["page"] = (offset / PAGE_SIZE.toFloat()).toIntUp().toString()
|
||||
payload["vars[meta_key]"] = when (sortOrder) {
|
||||
SortOrder.POPULARITY -> "_wp_manga_views"
|
||||
SortOrder.UPDATED -> "_latest_update"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class TemporaryCookieJar : CookieJar {
|
||||
class TestCookieJar : CookieJar {
|
||||
|
||||
private val cache = HashMap<CookieKey, Cookie>()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.KoinTestRule
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.repositoryTestModule
|
||||
import org.koitharu.kotatsu.utils.CoroutineTestRule
|
||||
import org.koitharu.kotatsu.utils.TestResponse
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
import org.koitharu.kotatsu.utils.ext.medianOrNull
|
||||
import org.koitharu.kotatsu.utils.isAbsoluteUrl
|
||||
import org.koitharu.kotatsu.utils.isRelativeUrl
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest {
|
||||
|
||||
private val repo by inject<RemoteMangaRepository> {
|
||||
parametersOf(source)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val koinTestRule = KoinTestRule.create {
|
||||
printLogger()
|
||||
modules(repositoryTestModule)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val coroutineTestRule = CoroutineTestRule()
|
||||
|
||||
@Test
|
||||
fun list() = coroutineTestRule.runBlockingTest {
|
||||
val list = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null)
|
||||
checkMangaList(list)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search() = coroutineTestRule.runBlockingTest {
|
||||
val subject = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null)
|
||||
.first()
|
||||
val list = repo.getList(offset = 0, query = subject.title, sortOrder = null, tag = null)
|
||||
checkMangaList(list)
|
||||
Truth.assertThat(list.map { it.url }).contains(subject.url)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tags() = coroutineTestRule.runBlockingTest {
|
||||
val tags = repo.getTags()
|
||||
Truth.assertThat(tags).isNotEmpty()
|
||||
val keys = tags.map { it.key }
|
||||
Truth.assertThat(keys).containsNoDuplicates()
|
||||
Truth.assertThat(keys).doesNotContain("")
|
||||
val titles = tags.map { it.title }
|
||||
Truth.assertThat(titles).containsNoDuplicates()
|
||||
Truth.assertThat(titles).doesNotContain("")
|
||||
Truth.assertThat(tags.mapToSet { it.source }).containsExactly(source)
|
||||
|
||||
val list = repo.getList(offset = 0, tag = tags.last(), query = null, sortOrder = null)
|
||||
checkMangaList(list)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun details() = coroutineTestRule.runBlockingTest {
|
||||
val list = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null)
|
||||
val item = list.first()
|
||||
val details = repo.getDetails(item)
|
||||
|
||||
Truth.assertThat(details.chapters).isNotEmpty()
|
||||
Truth.assertThat(details.publicUrl).isAbsoluteUrl()
|
||||
Truth.assertThat(details.description).isNotNull()
|
||||
Truth.assertThat(details.title).startsWith(item.title)
|
||||
Truth.assertThat(details.source).isEqualTo(source)
|
||||
|
||||
Truth.assertThat(details.chapters?.map { it.id }).containsNoDuplicates()
|
||||
Truth.assertThat(details.chapters?.map { it.number }).containsNoDuplicates()
|
||||
Truth.assertThat(details.chapters?.map { it.name }).doesNotContain("")
|
||||
Truth.assertThat(details.chapters?.mapToSet { it.source }).containsExactly(source)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pages() = coroutineTestRule.runBlockingTest {
|
||||
val list = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null)
|
||||
val chapter =
|
||||
repo.getDetails(list.first()).chapters?.firstOrNull() ?: error("Chapter is null")
|
||||
val pages = repo.getPages(chapter)
|
||||
|
||||
Truth.assertThat(pages).isNotEmpty()
|
||||
Truth.assertThat(pages.map { it.id }).containsNoDuplicates()
|
||||
Truth.assertThat(pages.mapToSet { it.source }).containsExactly(source)
|
||||
|
||||
val page = pages.medianOrNull() ?: error("No page")
|
||||
val pageUrl = repo.getPageUrl(page)
|
||||
Truth.assertThat(pageUrl).isNotEmpty()
|
||||
Truth.assertThat(pageUrl).isAbsoluteUrl()
|
||||
val pageResponse = TestResponse.testRequest(pageUrl)
|
||||
Truth.assertThat(pageResponse.code).isIn(200..299)
|
||||
Truth.assertThat(pageResponse.type).isEqualTo("image")
|
||||
}
|
||||
|
||||
private fun checkMangaList(list: List<Manga>) {
|
||||
Truth.assertThat(list).isNotEmpty()
|
||||
Truth.assertThat(list.map { it.id }).containsNoDuplicates()
|
||||
for (item in list) {
|
||||
Truth.assertThat(item.url).isNotEmpty()
|
||||
Truth.assertThat(item.url).isRelativeUrl()
|
||||
Truth.assertThat(item.coverUrl).isAbsoluteUrl()
|
||||
Truth.assertThat(item.title).isNotEmpty()
|
||||
Truth.assertThat(item.publicUrl).isAbsoluteUrl()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun getProviders() = (MangaSource.values().toList() - MangaSource.LOCAL).toTypedArray()
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,16 @@ import okhttp3.OkHttpClient
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.network.TestCookieJar
|
||||
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.SourceSettingsStub
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
val repositoryTestModule
|
||||
get() = module {
|
||||
single<CookieJar> { TemporaryCookieJar() }
|
||||
single<CookieJar> { TestCookieJar() }
|
||||
factory {
|
||||
OkHttpClient.Builder()
|
||||
.cookieJar(get())
|
||||
@@ -25,7 +27,7 @@ val repositoryTestModule
|
||||
single<MangaLoaderContext> {
|
||||
object : MangaLoaderContext(get(), get()) {
|
||||
override fun getSettings(source: MangaSource): SourceSettings {
|
||||
return SourceSettingsMock()
|
||||
return SourceSettingsStub()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
|
||||
class SourceSettingsMock : SourceSettings {
|
||||
class SourceSettingsStub : SourceSettings {
|
||||
|
||||
override fun getDomain(defaultValue: String) = defaultValue
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.KoinTestRule
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.utils.AssertX
|
||||
import org.koitharu.kotatsu.utils.ext.isDistinctBy
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class RemoteRepositoryTest(source: MangaSource) : KoinTest {
|
||||
|
||||
private val repo by inject<RemoteMangaRepository> {
|
||||
parametersOf(source)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val koinTestRule = KoinTestRule.create {
|
||||
printLogger()
|
||||
modules(repositoryTestModule)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun list() {
|
||||
val list = runBlocking { repo.getList(60) }
|
||||
Assert.assertFalse("List is empty", list.isEmpty())
|
||||
Assert.assertTrue("Mangas are not distinct", list.isDistinctBy { it.id })
|
||||
val item = list.random()
|
||||
AssertX.assertUrlRelative("Url is not relative", item.url)
|
||||
AssertX.assertUrlAbsolute("Url is not absolute", item.coverUrl)
|
||||
AssertX.assertContentType("Bad cover at ${item.url}", item.coverUrl, "image/*")
|
||||
AssertX.assertContentType(
|
||||
"invalid public url ${item.publicUrl}",
|
||||
item.publicUrl,
|
||||
"text/html"
|
||||
)
|
||||
Assert.assertFalse("Title is blank at ${item.url}", item.title.isBlank())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search() {
|
||||
val list = runBlocking { repo.getList(0, query = "tail") }
|
||||
Assert.assertFalse("List is empty", list.isEmpty())
|
||||
Assert.assertTrue("Mangas are not distinct", list.isDistinctBy { it.id })
|
||||
val nextList = runBlocking { repo.getList(list.size, query = "tail") }
|
||||
Assert.assertNotEquals("Search pagination is broken", list, nextList)
|
||||
val item = list.random()
|
||||
AssertX.assertUrlRelative("Url is not relative", item.url)
|
||||
AssertX.assertContentType("Bad cover at ${item.url}", item.coverUrl, "image/*")
|
||||
AssertX.assertContentType(
|
||||
"invalid public url ${item.publicUrl}",
|
||||
item.publicUrl,
|
||||
"text/html"
|
||||
)
|
||||
Assert.assertFalse("Title is blank at ${item.url}", item.title.isBlank())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tags() {
|
||||
val tags = runBlocking { repo.getTags() }
|
||||
Assert.assertFalse("No tags found", tags.isEmpty())
|
||||
val tag = tags.random()
|
||||
Assert.assertFalse("Tag title is blank for $tag", tag.key.isBlank())
|
||||
Assert.assertFalse("Tag title is blank for $tag", tag.title.isBlank())
|
||||
val list = runBlocking { repo.getList(0, tag = tag) }
|
||||
Assert.assertFalse("List is empty", list.isEmpty())
|
||||
val item = list.random()
|
||||
AssertX.assertUrlRelative("Url is not relative", item.url)
|
||||
AssertX.assertContentType("Bad cover at ${item.coverUrl}", item.coverUrl, "image/*")
|
||||
AssertX.assertContentType(
|
||||
"invalid public url ${item.publicUrl}",
|
||||
item.publicUrl,
|
||||
"text/html"
|
||||
)
|
||||
Assert.assertFalse("Title is blank at ${item.url}", item.title.isBlank())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun details() {
|
||||
val manga = runBlocking { repo.getList(0) }.random()
|
||||
val details = runBlocking { repo.getDetails(manga) }
|
||||
Assert.assertFalse("No chapters at ${details.url}", details.chapters.isNullOrEmpty())
|
||||
AssertX.assertContentType(
|
||||
"invalid public url ${details.publicUrl}",
|
||||
details.publicUrl,
|
||||
"text/html"
|
||||
)
|
||||
Assert.assertFalse(
|
||||
"Description is empty at ${details.url}",
|
||||
details.description.isNullOrEmpty()
|
||||
)
|
||||
Assert.assertTrue(
|
||||
"Chapters are not distinct",
|
||||
details.chapters.orEmpty().isDistinctBy { it.id })
|
||||
val chapter = details.chapters?.randomOrNull() ?: return
|
||||
AssertX.assertUrlRelative("Url is not relative", chapter.url)
|
||||
Assert.assertFalse(
|
||||
"Chapter name missing at ${details.url}:${chapter.number}",
|
||||
chapter.name.isBlank()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pages() {
|
||||
val manga = runBlocking { repo.getList(0) }.random()
|
||||
val details = runBlocking { repo.getDetails(manga) }
|
||||
val chapter = checkNotNull(details.chapters?.randomOrNull()) {
|
||||
"No chapters at ${details.url}"
|
||||
}
|
||||
val pages = runBlocking { repo.getPages(chapter) }
|
||||
Assert.assertFalse("Cannot find any page at ${chapter.url}", pages.isEmpty())
|
||||
Assert.assertTrue("Pages are not distinct", pages.isDistinctBy { it.id })
|
||||
val page = pages.randomOrNull() ?: return
|
||||
val fullUrl = runBlocking { repo.getPageUrl(page) }
|
||||
AssertX.assertContentType("Wrong page response from $fullUrl", fullUrl, "image/*")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun getProviders() = (MangaSource.values().toList() - MangaSource.LOCAL).toTypedArray()
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.junit.Assert
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
|
||||
object AssertX : KoinComponent {
|
||||
|
||||
private val okHttp by inject<OkHttpClient>()
|
||||
|
||||
fun assertContentType(message: String, url: String, vararg types: String) {
|
||||
Assert.assertFalse("URL is empty: $message", url.isEmpty())
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.head()
|
||||
.build()
|
||||
val response = okHttp.newCall(request).execute()
|
||||
when (val code = response.code) {
|
||||
HttpURLConnection.HTTP_OK -> {
|
||||
val type = response.body!!.contentType()
|
||||
Assert.assertTrue(types.any {
|
||||
val x = it.split('/')
|
||||
type?.type == x[0] && (x[1] == "*" || type.subtype == x[1])
|
||||
})
|
||||
}
|
||||
else -> Assert.fail("Invalid response code $code at $url: $message")
|
||||
}
|
||||
}
|
||||
|
||||
fun assertUrlRelative(message: String, url: String) {
|
||||
Assert.assertFalse(message, URI(url).isAbsolute)
|
||||
}
|
||||
|
||||
fun assertUrlAbsolute(message: String, url: String) {
|
||||
Assert.assertTrue(message, URI(url).isAbsolute)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
|
||||
class CoroutineTestRule(
|
||||
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(),
|
||||
) : TestWatcher() {
|
||||
|
||||
override fun starting(description: Description?) {
|
||||
super.starting(description)
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
override fun finished(description: Description?) {
|
||||
super.finished(description)
|
||||
Dispatchers.resetMain()
|
||||
testDispatcher.cleanupTestCoroutines()
|
||||
}
|
||||
|
||||
fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) {
|
||||
runBlocking(testDispatcher) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/src/test/java/org/koitharu/kotatsu/utils/TestResponse.kt
Normal file
32
app/src/test/java/org/koitharu/kotatsu/utils/TestResponse.kt
Normal file
@@ -0,0 +1,32 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
data class TestResponse(
|
||||
val code: Int,
|
||||
val type: String?,
|
||||
val subtype: String?,
|
||||
) {
|
||||
|
||||
companion object : KoinComponent {
|
||||
|
||||
private val okHttp by inject<OkHttpClient>()
|
||||
|
||||
fun testRequest(url: String): TestResponse {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.head()
|
||||
.build()
|
||||
val response = okHttp.newCall(request).execute()
|
||||
val type = response.body?.contentType()
|
||||
return TestResponse(
|
||||
code = response.code,
|
||||
type = type?.type,
|
||||
subtype = type?.subtype,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
11
app/src/test/java/org/koitharu/kotatsu/utils/TruthExt.kt
Normal file
11
app/src/test/java/org/koitharu/kotatsu/utils/TruthExt.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import com.google.common.truth.StringSubject
|
||||
import java.util.regex.Pattern
|
||||
|
||||
private val PATTERN_URL_ABSOLUTE = Pattern.compile("https?://[^\\s]+", Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_URL_RELATIVE = Pattern.compile("^/[^\\s]+", Pattern.CASE_INSENSITIVE)
|
||||
|
||||
fun StringSubject.isRelativeUrl() = matches(PATTERN_URL_RELATIVE)
|
||||
|
||||
fun StringSubject.isAbsoluteUrl() = matches(PATTERN_URL_ABSOLUTE)
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
Reference in New Issue
Block a user