Unit tests for json (de)serialization

This commit is contained in:
Koitharu
2022-07-18 09:19:29 +03:00
parent 2d4c1b751e
commit 9eb5e699e1
15 changed files with 300 additions and 194 deletions

View File

@@ -123,6 +123,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20220320'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.test:runner:1.4.0'

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
),
]
)
class BookmarkEntity(
data class BookmarkEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "page_id", index = true) val pageId: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long,

View File

@@ -1,14 +1,12 @@
package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.mapJSON
private const val PAGE_SIZE = 10
@@ -24,11 +22,11 @@ class BackupRepository(private val db: MangaDatabase) {
}
offset += history.size
for (item in history) {
val manga = item.manga.toJson()
val manga = JsonSerializer(item.manga).toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) }
item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
manga.put("tags", tags)
val json = item.history.toJson()
val json = JsonSerializer(item.history).toJson()
json.put("manga", manga)
entry.data.put(json)
}
@@ -40,7 +38,7 @@ class BackupRepository(private val db: MangaDatabase) {
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
val categories = db.favouriteCategoriesDao.findAll()
for (item in categories) {
entry.data.put(item.toJson())
entry.data.put(JsonSerializer(item).toJson())
}
return entry
}
@@ -55,11 +53,11 @@ class BackupRepository(private val db: MangaDatabase) {
}
offset += favourites.size
for (item in favourites) {
val manga = item.manga.toJson()
val manga = JsonSerializer(item.manga).toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) }
item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
manga.put("tags", tags)
val json = item.favourite.toJson()
val json = JsonSerializer(item.favourite).toJson()
json.put("manga", manga)
entry.data.put(json)
}
@@ -77,60 +75,54 @@ class BackupRepository(private val db: MangaDatabase) {
return entry
}
private fun MangaEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("id", id)
jo.put("title", title)
jo.put("alt_title", altTitle)
jo.put("url", url)
jo.put("public_url", publicUrl)
jo.put("rating", rating)
jo.put("nsfw", isNsfw)
jo.put("cover_url", coverUrl)
jo.put("large_cover_url", largeCoverUrl)
jo.put("state", state)
jo.put("author", author)
jo.put("source", source)
return jo
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = mangaJson.getJSONArray("tags").mapJSON {
JsonDeserializer(it).toTagEntity()
}
val history = JsonDeserializer(item).toHistoryEntity()
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.historyDao.upsert(history)
}
}
}
return result
}
private fun TagEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("id", id)
jo.put("title", title)
jo.put("key", key)
jo.put("source", source)
return jo
suspend fun restoreCategories(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
result += runCatching {
db.favouriteCategoriesDao.upsert(category)
}
}
return result
}
private fun HistoryEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("manga_id", mangaId)
jo.put("created_at", createdAt)
jo.put("updated_at", updatedAt)
jo.put("chapter_id", chapterId)
jo.put("page", page)
jo.put("scroll", scroll)
jo.put("percent", percent)
return jo
}
private fun FavouriteCategoryEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("category_id", categoryId)
jo.put("created_at", createdAt)
jo.put("sort_key", sortKey)
jo.put("title", title)
jo.put("order", order)
jo.put("track", track)
return jo
}
private fun FavouriteEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("manga_id", mangaId)
jo.put("category_id", categoryId)
jo.put("created_at", createdAt)
return jo
suspend fun restoreFavourites(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = mangaJson.getJSONArray("tags").mapJSON {
JsonDeserializer(it).toTagEntity()
}
val favourite = JsonDeserializer(item).toFavouriteEntity()
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.favouritesDao.upsert(favourite)
}
}
}
return result
}
}

View File

@@ -0,0 +1,62 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
class JsonDeserializer(private val json: JSONObject) {
fun toFavouriteEntity() = FavouriteEntity(
mangaId = json.getLong("manga_id"),
categoryId = json.getLong("category_id"),
createdAt = json.getLong("created_at"),
)
fun toMangaEntity() = MangaEntity(
id = json.getLong("id"),
title = json.getString("title"),
altTitle = json.getStringOrNull("alt_title"),
url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(),
rating = json.getDouble("rating").toFloat(),
isNsfw = json.getBooleanOrDefault("nsfw", false),
coverUrl = json.getString("cover_url"),
largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"),
source = json.getString("source")
)
fun toTagEntity() = TagEntity(
id = json.getLong("id"),
title = json.getString("title"),
key = json.getString("key"),
source = json.getString("source")
)
fun toHistoryEntity() = HistoryEntity(
mangaId = json.getLong("manga_id"),
createdAt = json.getLong("created_at"),
updatedAt = json.getLong("updated_at"),
chapterId = json.getLong("chapter_id"),
page = json.getInt("page"),
scroll = json.getDouble("scroll").toFloat(),
percent = json.getFloatOrDefault("percent", -1f),
)
fun toFavouriteCategoryEntity() = FavouriteCategoryEntity(
categoryId = json.getInt("category_id"),
createdAt = json.getLong("created_at"),
sortKey = json.getInt("sort_key"),
title = json.getString("title"),
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
track = json.getBooleanOrDefault("track", true),
)
}

View File

@@ -0,0 +1,70 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
class JsonSerializer private constructor(private val json: JSONObject) {
constructor(e: FavouriteEntity) : this(
JSONObject().apply {
put("manga_id", e.mangaId)
put("category_id", e.categoryId)
put("created_at", e.createdAt)
}
)
constructor(e: FavouriteCategoryEntity) : this(
JSONObject().apply {
put("category_id", e.categoryId)
put("created_at", e.createdAt)
put("sort_key", e.sortKey)
put("title", e.title)
put("order", e.order)
put("track", e.track)
}
)
constructor(e: HistoryEntity) : this(
JSONObject().apply {
put("manga_id", e.mangaId)
put("created_at", e.createdAt)
put("updated_at", e.updatedAt)
put("chapter_id", e.chapterId)
put("page", e.page)
put("scroll", e.scroll)
put("percent", e.percent)
}
)
constructor(e: TagEntity) : this(
JSONObject().apply {
put("id", e.id)
put("title", e.title)
put("key", e.key)
put("source", e.source)
}
)
constructor(e: MangaEntity) : this(
JSONObject().apply {
put("id", e.id)
put("title", e.title)
put("alt_title", e.altTitle)
put("url", e.url)
put("public_url", e.publicUrl)
put("rating", e.rating)
put("nsfw", e.isNsfw)
put("cover_url", e.coverUrl)
put("large_cover_url", e.largeCoverUrl)
put("state", e.state)
put("author", e.author)
put("source", e.source)
}
)
fun toJson(): JSONObject = json
}

View File

@@ -1,113 +0,0 @@
package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.json.*
class RestoreRepository(private val db: MangaDatabase) {
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").mapJSON {
parseTag(it)
}
val history = parseHistory(item)
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.historyDao.upsert(history)
}
}
}
return result
}
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val category = parseCategory(item)
result += runCatching {
db.favouriteCategoriesDao.upsert(category)
}
}
return result
}
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").mapJSON {
parseTag(it)
}
val favourite = parseFavourite(item)
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.favouritesDao.upsert(favourite)
}
}
}
return result
}
private fun parseManga(json: JSONObject) = MangaEntity(
id = json.getLong("id"),
title = json.getString("title"),
altTitle = json.getStringOrNull("alt_title"),
url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(),
rating = json.getDouble("rating").toFloat(),
isNsfw = json.getBooleanOrDefault("nsfw", false),
coverUrl = json.getString("cover_url"),
largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"),
source = json.getString("source")
)
private fun parseTag(json: JSONObject) = TagEntity(
id = json.getLong("id"),
title = json.getString("title"),
key = json.getString("key"),
source = json.getString("source")
)
private fun parseHistory(json: JSONObject) = HistoryEntity(
mangaId = json.getLong("manga_id"),
createdAt = json.getLong("created_at"),
updatedAt = json.getLong("updated_at"),
chapterId = json.getLong("chapter_id"),
page = json.getInt("page"),
scroll = json.getDouble("scroll").toFloat(),
percent = json.getFloatOrDefault("percent", -1f),
)
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity(
categoryId = json.getInt("category_id"),
createdAt = json.getLong("created_at"),
sortKey = json.getInt("sort_key"),
title = json.getString("title"),
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
track = json.getBooleanOrDefault("track", true),
)
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
mangaId = json.getLong("manga_id"),
categoryId = json.getLong("category_id"),
createdAt = json.getLong("created_at")
)
}

View File

@@ -5,7 +5,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "manga")
class MangaEntity(
data class MangaEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String,

View File

@@ -5,7 +5,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tags")
class TagEntity(
data class TagEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "tag_id") val id: Long,
@ColumnInfo(name = "title") val title: String,

View File

@@ -5,7 +5,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "favourite_categories")
class FavouriteCategoryEntity(
data class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long,

View File

@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
)
]
)
class FavouriteEntity(
data class FavouriteEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
)
]
)
class HistoryEntity(
data class HistoryEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long,

View File

@@ -5,7 +5,6 @@ import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.RestoreRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
@@ -18,7 +17,6 @@ val settingsModule
get() = module {
factory { BackupRepository(get()) }
factory { RestoreRepository(get()) }
single(createdAtStart = true) { AppSettings(androidContext()) }
viewModel { BackupViewModel(get(), androidContext()) }

View File

@@ -6,7 +6,10 @@ import android.app.backup.BackupDataOutput
import android.app.backup.FullBackupDataOutput
import android.os.ParcelFileDescriptor
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.core.backup.*
import org.koitharu.kotatsu.core.backup.BackupEntry
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.BackupZipInput
import org.koitharu.kotatsu.core.backup.BackupZipOutput
import org.koitharu.kotatsu.core.db.MangaDatabase
import java.io.*
@@ -63,7 +66,7 @@ class AppBackupAgent : BackupAgent() {
}
private fun restoreBackupFile(fd: FileDescriptor, size: Long) {
val repository = RestoreRepository(MangaDatabase(applicationContext))
val repository = BackupRepository(MangaDatabase(applicationContext))
val tempFile = File.createTempFile("backup_", ".tmp")
FileInputStream(fd).use { input ->
tempFile.outputStream().use { output ->
@@ -73,9 +76,9 @@ class AppBackupAgent : BackupAgent() {
val backup = BackupZipInput(tempFile)
try {
runBlocking {
repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY))
repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES))
repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES))
repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
}
} finally {
backup.close()

View File

@@ -7,9 +7,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.backup.BackupEntry
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.BackupZipInput
import org.koitharu.kotatsu.core.backup.CompositeResult
import org.koitharu.kotatsu.core.backup.RestoreRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File
@@ -17,7 +17,7 @@ import java.io.FileNotFoundException
class RestoreViewModel(
uri: Uri?,
private val repository: RestoreRepository,
private val repository: BackupRepository,
context: Context
) : BaseViewModel() {
@@ -44,13 +44,13 @@ class RestoreViewModel(
val result = CompositeResult()
progress.value = Progress(0, 3)
result += repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY))
result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
progress.value = Progress(1, 3)
result += repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES))
result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
progress.value = Progress(2, 3)
result += repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES))
result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
progress.value = Progress(3, 3)
onRestoreDone.call(result)

View File

@@ -0,0 +1,93 @@
package org.koitharu.kotatsu.core.backup
import org.junit.Assert.assertEquals
import org.junit.Test
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.concurrent.TimeUnit
class JsonSerializerTest {
@Test
fun toFavouriteEntity() {
val entity = FavouriteEntity(
mangaId = 40,
categoryId = 20,
createdAt = System.currentTimeMillis(),
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toFavouriteEntity()
assertEquals(entity, result)
}
@Test
fun toMangaEntity() {
val entity = MangaEntity(
id = 231,
title = "Lorem Ipsum",
altTitle = "Lorem Ispum 2",
url = "erw",
publicUrl = "hthth",
rating = 0.78f,
isNsfw = true,
coverUrl = "5345",
largeCoverUrl = null,
state = MangaState.FINISHED.name,
author = "RERE",
source = MangaSource.DUMMY.name,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toMangaEntity()
assertEquals(entity, result)
}
@Test
fun toTagEntity() {
val entity = TagEntity(
id = 934023534,
title = "Adventure",
key = "adventure",
source = MangaSource.DUMMY.name,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toTagEntity()
assertEquals(entity, result)
}
@Test
fun toHistoryEntity() {
val entity = HistoryEntity(
mangaId = 304135341,
createdAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(6),
updatedAt = System.currentTimeMillis(),
chapterId = 29014843034,
page = 35,
scroll = 24.0f,
percent = 0.6f,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toHistoryEntity()
assertEquals(entity, result)
}
@Test
fun toFavouriteCategoryEntity() {
val entity = FavouriteCategoryEntity(
categoryId = 142,
createdAt = System.currentTimeMillis(),
sortKey = 14,
title = "Read later",
order = SortOrder.RATING.name,
track = false,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toFavouriteCategoryEntity()
assertEquals(entity, result)
}
}