Repository Unit Tests #50

Merged
AmineB merged 38 commits from davidoskky/ReaderForSelfoss-multiplatform:repository_tests into master 2022-09-30 11:31:55 +00:00
7 changed files with 1064 additions and 36 deletions

View File

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

View File

@ -355,6 +355,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val drawerData = DrawerData(repository.getDBTags().map { it.toView() },
repository.getDBSources().map { it.toView() })
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
if (drawerData.sources?.isEmpty() == true || appSettingsService.isUpdateSourcesEnabled()) {
drawerApiCalls(drawerData)

View File

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

View File

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

View File

@ -56,6 +56,8 @@ kotlin {
dependencies {
implementation(kotlin("test-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 {

View File

@ -11,6 +11,7 @@ import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
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
set(value) {field = if (value < 0) { 0 } else { value } }
private var fetchedSources = false
private var fetchedTags = false
AmineB marked this conversation as resolved Outdated

What does this change do ?

What does this change do ?

This does not change the behavior of the function at all.
runBlocking and Dispatchers.Main are basically the same thing.
However, the testing framework for the common code does not support Dispatchers.
This edit serves pretty much just to make the tests work properly.

I'm certain there is some way to make it work keeping the Dispatchers in there, but since these are equivalent I preferred saving some time and just slightly change the function.

This does not change the behavior of the function at all. runBlocking and Dispatchers.Main are basically the same thing. However, the testing framework for the common code does not support Dispatchers. This edit serves pretty much just to make the tests work properly. I'm certain there is some way to make it work keeping the Dispatchers in there, but since these are equivalent I preferred saving some time and just slightly change the function.
init {
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
CoroutineScope(Dispatchers.Main).launch {
runBlocking {
updateApiVersion()
dateUtils = DateUtils(appSettingsService)
reloadBadges()
@ -105,9 +109,9 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
val items = api.getItems(
itemType.type,
0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null,
null,
null,
null,
200
)
@ -131,32 +135,40 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
badgeStarred = response.data.starred
success = true
}
} else {
} else if (appSettingsService.isItemCachingEnabled()) {
// TODO: do this differently, because it's not efficient
val dbItems = getDBItems()
badgeUnread = dbItems.filter { item -> item.unread }.size
badgeStarred = dbItems.filter { item -> item.starred }.size
badgeAll = items.size
badgeAll = dbItems.size
success = true
}
return success
}
suspend fun getTags(): List<SelfossModel.Tag>? {
return if (isNetworkAvailable()) {
suspend fun getTags(): List<SelfossModel.Tag> {
val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
return if (isNetworkAvailable() && !fetchedTags) {
val apiTags = api.tags()
if (apiTags.success && apiTags.data != null && (appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled())) {
if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
resetDBTagsWithData(apiTags.data)
if (!appSettingsService.isUpdateSourcesEnabled()) {
fetchedTags = true
}
}
apiTags.data
} else {
apiTags.data ?: emptyList()
} else if (isDatabaseEnabled) {
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()) {
val spouts = api.spouts()
return if (spouts.success && spouts.data != null) {
if (spouts.success && spouts.data != null) {
spouts.data
} else {
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>? {
return if (isNetworkAvailable()) {
suspend fun getSources(): ArrayList<SelfossModel.Source> {
val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
return if (isNetworkAvailable() && !fetchedSources) {
val apiSources = api.sources()
if (apiSources.success && apiSources.data != null && (appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled())) {
if (apiSources.success && apiSources.data != null && isDatabaseEnabled) {
resetDBSourcesWithData(apiSources.data)
if (!appSettingsService.isUpdateSourcesEnabled()) {
fetchedSources = true
}
}
apiSources.data
} else {
apiSources.data ?: ArrayList()
} else if (isDatabaseEnabled) {
ArrayList(getDBSources().map { it.toView() })
} else {
ArrayList()
}
}
// TODO: Add tests
suspend fun markAsRead(item: SelfossModel.Item): Boolean {
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 {
return if (isNetworkAvailable()) {
api.markAsRead(id.toString())?.isSuccess
api.markAsRead(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), read = true)
true
}
}
// TODO: Add tests
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
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 {
return if (isNetworkAvailable()) {
api.unmarkAsRead(id.toString())?.isSuccess
api.unmarkAsRead(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), unread = true)
true
}
}
// TODO: Add tests
suspend fun starr(item: SelfossModel.Item): Boolean {
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 {
return if (isNetworkAvailable()) {
api.starr(id.toString())?.isSuccess
api.starr(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), starred = true)
true
}
}
// TODO: Add tests
suspend fun unstarr(item: SelfossModel.Item): Boolean {
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 {
return if (isNetworkAvailable()) {
api.unstarr(id.toString())?.isSuccess
api.unstarr(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), starred = true)
true
}
}
// TODO: Add tests
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
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
for (item in items) {
markAsReadLocally(item)
@ -323,7 +345,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
tags,
filter,
appSettingsService.getApiVersion()
)?.isSuccess == true
).isSuccess == true
}
return response
@ -333,9 +355,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
var success = false
if (isNetworkAvailable()) {
val response = api.deleteSource(id)
if (response != null) {
success = response.isSuccess
}
success = response.isSuccess
}
return success
@ -343,7 +363,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
suspend fun updateRemote(): Boolean {
return if (isNetworkAvailable()) {
api.update()?.equals("finished")
api.update().data.equals("finished")
} else {
false
}
@ -354,7 +374,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
if (isNetworkAvailable()) {
try {
val response = api.login()
result = response?.isSuccess == true
result = response.isSuccess == true
if (result) {
updateApiVersion()
}
@ -371,7 +391,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
api.refreshLoginInformation()
}
suspend fun updateApiVersion() {
private suspend fun updateApiVersion() {
val apiMajorVersion = appSettingsService.getApiVersion()
if (isNetworkAvailable()) {
@ -390,8 +410,10 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private fun deleteDBAction(action: ACTION) =
db.actionsQueries.deleteAction(action.id)
// TODO: This function should be private
fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList()
// TODO: This function should be private
fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList()
AmineB marked this conversation as resolved Outdated

If it's not used elsewere, please remove the todo, and change the function to private.

Same for the other todos.

If it's not used elsewere, please remove the todo, and change the function to private. Same for the other todos.

getDBTags and getDBSources are used in the home activity.
It would be better in my opinion to remove this direct access to the database and let the repository check all the logical steps.
I feel that refactoring that is a bit out of the scope of this PR though.

getDBTags and getDBSources are used in the home activity. It would be better in my opinion to remove this direct access to the database and let the repository check all the logical steps. I feel that refactoring that is a bit out of the scope of this PR though.

Yes. Could you add a comment in the home activity, the refactoring will be done later.

Yes. Could you add a comment in the home activity, the refactoring will be done later.
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) =
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 {
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
val allItems = getMaxItemsForBackground(ItemType.ALL)
AmineB marked this conversation as resolved Outdated

I think that the DB handles the duplicates.

I think that the DB handles the duplicates.

Does it use the id to check for duplicates? If that is the case then all is good.

Does it use the id to check for duplicates? If that is the case then all is good.
@ -444,6 +465,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
return emptyList()
}
// TODO: Add tests
suspend fun handleDBActions() {
val actions: List<ACTION> = getDBActions()