Merge pull request 'Repository Unit Tests' (#50) from davidoskky/ReaderForSelfoss-multiplatform:repository_tests into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/50
This commit is contained in:
Amine Louveau 2022-09-30 11:31:55 +00:00
commit 6ec3e96909
7 changed files with 1064 additions and 36 deletions

View File

@ -15,6 +15,7 @@ steps:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Testing..." - echo "Testing..."
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- ./gradlew test -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false
environment: environment:
SONAR_HOST_URL: SONAR_HOST_URL:
from_secret: sonarScannerHostUrl from_secret: sonarScannerHostUrl

View File

@ -355,6 +355,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val drawerData = DrawerData(repository.getDBTags().map { it.toView() }, val drawerData = DrawerData(repository.getDBTags().map { it.toView() },
repository.getDBSources().map { it.toView() }) repository.getDBSources().map { it.toView() })
runOnUiThread { runOnUiThread {
// TODO: All this logic should be handled by the repository, simplify and remove direct DB access
// Only refresh if there is no data in the DB, or if the `UpdateSources` setting is enabled // Only refresh if there is no data in the DB, or if the `UpdateSources` setting is enabled
if (drawerData.sources?.isEmpty() == true || appSettingsService.isUpdateSourcesEnabled()) { if (drawerData.sources?.isEmpty() == true || appSettingsService.isUpdateSourcesEnabled()) {
drawerApiCalls(drawerData) drawerApiCalls(drawerData)

View File

@ -163,7 +163,6 @@ class LoginActivity : AppCompatActivity(), DIAware {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val result = repository.login() val result = repository.login()
if (result) { if (result) {
repository.updateApiVersion()
goToMain() goToMain()
} else { } else {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {

View File

@ -52,11 +52,13 @@ override fun doWork(): Result {
repository.handleDBActions() repository.handleDBActions()
val apiItems = repository.tryToCacheItemsAndGetNewOnes()
if (appSettingsService.isNotifyNewItemsEnabled()) { if (appSettingsService.isNotifyNewItemsEnabled()) {
launch { launch {
handleNewItemsNotification(repository.tryToCacheItemsAndGetNewOnes(), notificationManager) handleNewItemsNotification(apiItems, notificationManager)
} }
} }
apiItems.map { it.preloadImages(context) }
} }
} }
return Result.success() return Result.success()
@ -66,6 +68,7 @@ override fun doWork(): Result {
newItems: List<SelfossModel.Item>?, newItems: List<SelfossModel.Item>?,
notificationManager: NotificationManager notificationManager: NotificationManager
) { ) {
// TODO: Check if this coroutine is actually required
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val apiItems = newItems.orEmpty() val apiItems = newItems.orEmpty()
@ -102,7 +105,6 @@ override fun doWork(): Result {
notificationManager.notify(2, newItemsNotification.build()) notificationManager.notify(2, newItemsNotification.build())
} }
} }
apiItems.map { it.preloadImages(context) }
Timer("", false).schedule(4000) { Timer("", false).schedule(4000) {
notificationManager.cancel(1) notificationManager.cancel(1)
} }

View File

@ -56,6 +56,8 @@ kotlin {
dependencies { dependencies {
implementation(kotlin("test-common")) implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common")) implementation(kotlin("test-annotations-common"))
implementation("io.mockk:mockk:1.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
} }
} }
val androidMain by getting { val androidMain by getting {

View File

@ -11,6 +11,7 @@ import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) { class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) {
@ -36,9 +37,12 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
var badgeStarred = 0 var badgeStarred = 0
set(value) {field = if (value < 0) { 0 } else { value } } set(value) {field = if (value < 0) { 0 } else { value } }
private var fetchedSources = false
private var fetchedTags = false
init { init {
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found // TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
CoroutineScope(Dispatchers.Main).launch { runBlocking {
updateApiVersion() updateApiVersion()
dateUtils = DateUtils(appSettingsService) dateUtils = DateUtils(appSettingsService)
reloadBadges() reloadBadges()
@ -105,9 +109,9 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
val items = api.getItems( val items = api.getItems(
itemType.type, itemType.type,
0, 0,
tagFilter?.tag, null,
sourceFilter?.id?.toLong(), null,
searchFilter, null,
null, null,
200 200
) )
@ -131,32 +135,40 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
badgeStarred = response.data.starred badgeStarred = response.data.starred
success = true success = true
} }
} else { } else if (appSettingsService.isItemCachingEnabled()) {
// TODO: do this differently, because it's not efficient // TODO: do this differently, because it's not efficient
val dbItems = getDBItems() val dbItems = getDBItems()
badgeUnread = dbItems.filter { item -> item.unread }.size badgeUnread = dbItems.filter { item -> item.unread }.size
badgeStarred = dbItems.filter { item -> item.starred }.size badgeStarred = dbItems.filter { item -> item.starred }.size
badgeAll = items.size badgeAll = dbItems.size
success = true
} }
return success return success
} }
suspend fun getTags(): List<SelfossModel.Tag>? { suspend fun getTags(): List<SelfossModel.Tag> {
return if (isNetworkAvailable()) { val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
return if (isNetworkAvailable() && !fetchedTags) {
val apiTags = api.tags() val apiTags = api.tags()
if (apiTags.success && apiTags.data != null && (appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled())) { if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
resetDBTagsWithData(apiTags.data) resetDBTagsWithData(apiTags.data)
if (!appSettingsService.isUpdateSourcesEnabled()) {
fetchedTags = true
} }
apiTags.data }
} else { apiTags.data ?: emptyList()
} else if (isDatabaseEnabled) {
getDBTags().map { it.toView() } getDBTags().map { it.toView() }
} else {
emptyList()
} }
} }
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? { // TODO: Add tests
suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
val spouts = api.spouts() val spouts = api.spouts()
return if (spouts.success && spouts.data != null) { if (spouts.success && spouts.data != null) {
spouts.data spouts.data
} else { } else {
emptyMap() // TODO: do something here emptyMap() // TODO: do something here
@ -166,18 +178,25 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
} }
} }
suspend fun getSources(): ArrayList<SelfossModel.Source>? { suspend fun getSources(): ArrayList<SelfossModel.Source> {
return if (isNetworkAvailable()) { val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
return if (isNetworkAvailable() && !fetchedSources) {
val apiSources = api.sources() val apiSources = api.sources()
if (apiSources.success && apiSources.data != null && (appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled())) { if (apiSources.success && apiSources.data != null && isDatabaseEnabled) {
resetDBSourcesWithData(apiSources.data) resetDBSourcesWithData(apiSources.data)
if (!appSettingsService.isUpdateSourcesEnabled()) {
fetchedSources = true
} }
apiSources.data }
} else { apiSources.data ?: ArrayList()
} else if (isDatabaseEnabled) {
ArrayList(getDBSources().map { it.toView() }) ArrayList(getDBSources().map { it.toView() })
} else {
ArrayList()
} }
} }
// TODO: Add tests
suspend fun markAsRead(item: SelfossModel.Item): Boolean { suspend fun markAsRead(item: SelfossModel.Item): Boolean {
val success = markAsReadById(item.id) val success = markAsReadById(item.id)
@ -189,14 +208,14 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private suspend fun markAsReadById(id: Int): Boolean { private suspend fun markAsReadById(id: Int): Boolean {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.markAsRead(id.toString())?.isSuccess api.markAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), read = true) insertDBAction(id.toString(), read = true)
true true
} }
} }
// TODO: Add tests
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id) val success = unmarkAsReadById(item.id)
@ -208,13 +227,14 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private suspend fun unmarkAsReadById(id: Int): Boolean { private suspend fun unmarkAsReadById(id: Int): Boolean {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.unmarkAsRead(id.toString())?.isSuccess api.unmarkAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), unread = true) insertDBAction(id.toString(), unread = true)
true true
} }
} }
// TODO: Add tests
suspend fun starr(item: SelfossModel.Item): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id) val success = starrById(item.id)
@ -226,13 +246,14 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private suspend fun starrById(id: Int): Boolean { private suspend fun starrById(id: Int): Boolean {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.starr(id.toString())?.isSuccess api.starr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
true true
} }
} }
// TODO: Add tests
suspend fun unstarr(item: SelfossModel.Item): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id) val success = unstarrById(item.id)
@ -244,17 +265,18 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private suspend fun unstarrById(id: Int): Boolean { private suspend fun unstarrById(id: Int): Boolean {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.unstarr(id.toString())?.isSuccess api.unstarr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
true true
} }
} }
// TODO: Add tests
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false var success = false
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() })?.isSuccess) { if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess) {
success = true success = true
for (item in items) { for (item in items) {
markAsReadLocally(item) markAsReadLocally(item)
@ -323,7 +345,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
tags, tags,
filter, filter,
appSettingsService.getApiVersion() appSettingsService.getApiVersion()
)?.isSuccess == true ).isSuccess == true
} }
return response return response
@ -333,17 +355,15 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
var success = false var success = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
if (response != null) {
success = response.isSuccess success = response.isSuccess
} }
}
return success return success
} }
suspend fun updateRemote(): Boolean { suspend fun updateRemote(): Boolean {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.update()?.equals("finished") api.update().data.equals("finished")
} else { } else {
false false
} }
@ -354,7 +374,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
try { try {
val response = api.login() val response = api.login()
result = response?.isSuccess == true result = response.isSuccess == true
if (result) { if (result) {
updateApiVersion() updateApiVersion()
} }
@ -371,7 +391,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
api.refreshLoginInformation() api.refreshLoginInformation()
} }
suspend fun updateApiVersion() { private suspend fun updateApiVersion() {
val apiMajorVersion = appSettingsService.getApiVersion() val apiMajorVersion = appSettingsService.getApiVersion()
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
@ -390,8 +410,10 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private fun deleteDBAction(action: ACTION) = private fun deleteDBAction(action: ACTION) =
db.actionsQueries.deleteAction(action.id) db.actionsQueries.deleteAction(action.id)
// TODO: This function should be private
fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList() fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList()
// TODO: This function should be private
fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList() fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList()
private fun resetDBTagsWithData(tagEntities: List<SelfossModel.Tag>) { private fun resetDBTagsWithData(tagEntities: List<SelfossModel.Tag>) {
@ -430,8 +452,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private fun updateDBItem(item: SelfossModel.Item) = private fun updateDBItem(item: SelfossModel.Item) =
db.itemsQueries.updateItem(item.datetime, item.title.getHtmlDecoded(), item.content, item.unread, item.starred, item.thumbnail, item.icon, item.link, item.sourcetitle, item.tags.joinToString(","), item.id.toString()) db.itemsQueries.updateItem(item.datetime, item.title.getHtmlDecoded(), item.content, item.unread, item.starred, item.thumbnail, item.icon, item.link, item.sourcetitle, item.tags.joinToString(","), item.id.toString())
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item>? {
try { try {
val newItems = getMaxItemsForBackground(ItemType.UNREAD) val newItems = getMaxItemsForBackground(ItemType.UNREAD)
val allItems = getMaxItemsForBackground(ItemType.ALL) val allItems = getMaxItemsForBackground(ItemType.ALL)
@ -444,6 +465,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
return emptyList() return emptyList()
} }
// TODO: Add tests
suspend fun handleDBActions() { suspend fun handleDBActions() {
val actions: List<ACTION> = getDBActions() val actions: List<ACTION> = getDBActions()