Improve remote repository tests

This commit is contained in:
Koitharu
2021-07-15 19:54:22 +03:00
parent 6a3421df8a
commit d2609c0560
12 changed files with 213 additions and 185 deletions

View File

@@ -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'
}

View File

@@ -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"

View File

@@ -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>()

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View 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,
)
}
}
}

View 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)

View File

@@ -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