Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package com.loki.plitso.data.remote.bookmark

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.firebase.Firebase
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.firestore
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.coroutines.resume

@RunWith(AndroidJUnit4::class)
class BookmarkApiImplTest {
private val user = "testUser"
private val bookmarkApi = BookmarkApiImpl(user)

@Test
fun getBookmarkNoExistingDocument() =
runBlocking {
val bookmarks = withTimeoutOrNull(5000) { bookmarkApi.bookmarks.first() }
assertEquals(null, bookmarks)
}

@Test
fun getBookmarkExistingDocumentNoBookmarkField() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set("name" to "John Doe")
.addOnCompleteListener {
c.resume(Unit)
}
}
val bookmarks = bookmarkApi.bookmarks.first()
assertEquals(0, bookmarks.size)
}

@Test
fun getBookmarkExistingDocumentWithEmptyBookmarkField() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
BOOKMARKS_FIELD to emptyList<String>(),
),
)
.addOnCompleteListener {
c.resume(Unit)
}
}
val bookmarks = bookmarkApi.bookmarks.first()
assertEquals(0, bookmarks.size)
}

@Test
fun getBookmarkExistingDocumentWithPrepopulatedBookmarkField() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
BOOKMARKS_FIELD to listOf("1", "2"),
),
).addOnCompleteListener {
c.resume(Unit)
}
}
val bookmarks = bookmarkApi.bookmarks.first()
assertEquals(2, bookmarks.size)
}

@Test
fun addBookmarkNoExistingDocument() =
runBlocking {
val expected = "recipe1"
bookmarkApi.saveBookmark(expected)
val actual = bookmarkApi.bookmarks.first().first()
assertEquals(expected, actual)
}

@Test
fun addBookmarkOnExistingDocumentWithoutBookmarkField() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set("name" to "John Doe")
.addOnCompleteListener {
c.resume(Unit)
}
}
val expected = "recipe1"
bookmarkApi.saveBookmark(expected)
val actual = bookmarkApi.bookmarks.first().first()
assertEquals(expected, actual)
}

@Test
fun addBookmarkOnExistingDocumentWithBookmarkField() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
BOOKMARKS_FIELD to listOf("recipe12"),
),
)
.addOnCompleteListener {
c.resume(Unit)
}
}
val expected = "recipe1"
bookmarkApi.saveBookmark(expected)
val bookmarks = bookmarkApi.bookmarks.first()
assertEquals(2, bookmarks.size)
assert(bookmarks.contains(expected))
}

@Test
fun addSameBookmarkTwice() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
BOOKMARKS_FIELD to listOf("recipe12"),
),
)
.addOnCompleteListener {
c.resume(Unit)
}
}
val expected = "recipe12"
bookmarkApi.saveBookmark(expected)
val bookmarks = bookmarkApi.bookmarks.first()
assertEquals(1, bookmarks.size)
assertEquals(1, bookmarks.count { it == expected })
}

@Test
fun deleteBookmarkFromNonExistingDocument() =
runBlocking {
val recipeId = "recipe12"
val exception = runCatching { bookmarkApi.deleteBookmark(recipeId) }.exceptionOrNull()
assert(exception is FirebaseFirestoreException)
}

@Test
fun deleteBookmarkFromExistingDocumentWithNonExistingBookmarkField() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
),
)
.addOnCompleteListener {
c.resume(Unit)
}
}
val recipeId = "recipe12"
bookmarkApi.deleteBookmark(recipeId)
val bookmarks = bookmarkApi.bookmarks.first()
assertEquals(0, bookmarks.size)
}

@Test
fun deleteBookmarkWithExistingField() =
runBlocking {
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
BOOKMARKS_FIELD to listOf("recipe12", "rec2", "eiwe2"),
),
)
.addOnCompleteListener {
c.resume(Unit)
}
}
val recipeId = "recipe12"
bookmarkApi.deleteBookmark(recipeId)
val bookmarks = bookmarkApi.bookmarks.first()
assertEquals(2, bookmarks.size)
assertFalse(bookmarks.contains(recipeId))
}

@Test
fun isBookmarkedNoExistingDocument() =
runBlocking {
val recipeId = "recipe12"
val isBookmarked = withTimeoutOrNull(5000) { bookmarkApi.isBookmarked(recipeId) }
assertEquals(false, isBookmarked)
}

@Test
fun isBookmarkedExistingDocumentNoBookmarkField() =
runBlocking {
val recipeId = "recipe12"
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set("name" to "John Doe")
.addOnCompleteListener {
c.resume(Unit)
}
}
assertEquals(false, bookmarkApi.isBookmarked(recipeId = recipeId))
}

@Test
fun isBookmarkedExistingDocumentWithEmptyBookmarkField() =
runBlocking {
val recipeId = "recipe12"
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
BOOKMARKS_FIELD to emptyList<String>(),
),
)
.addOnCompleteListener {
c.resume(Unit)
}
}
assertEquals(false, bookmarkApi.isBookmarked(recipeId))
}

@Test
fun isBookmarkedExistingDocumentWithPrepopulatedBookmarkField() =
runBlocking {
val recipeId = "recipe12"
suspendCancellableCoroutine { c ->
Firebase.firestore.document("users/$user")
.set(
mapOf(
"name" to "John Doe",
BOOKMARKS_FIELD to listOf("1", "2", recipeId),
),
).addOnCompleteListener {
c.resume(Unit)
}
}
assertEquals(true, bookmarkApi.isBookmarked(recipeId))
}

@After
fun cleanUp() =
runBlocking {
suspendCancellableCoroutine { continuation ->
Firebase.firestore.document("users/$user").delete().addOnCompleteListener {
continuation.resume(Unit)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.loki.plitso.data.remote.bookmark

import kotlinx.coroutines.flow.Flow

interface BookmarkApi {
val bookmarks: Flow<List<String>>

suspend fun saveBookmark(recipeId: String)

suspend fun deleteBookmark(recipeId: String)

suspend fun isBookmarked(recipeId: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.loki.plitso.data.remote.bookmark

import com.google.firebase.Firebase
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.SetOptions
import com.google.firebase.firestore.firestore
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

const val BOOKMARKS_FIELD = "bookmarks"

class BookmarkApiImpl(userId: String) : BookmarkApi {
private val bookmarkRef = Firebase.firestore.document("users/$userId")

override val bookmarks: Flow<List<String>>
get() {
return callbackFlow {
val listenerRegistration =
bookmarkRef.addSnapshotListener { snapshot, error ->
if (error != null) {
close(error)
return@addSnapshotListener
}
if (snapshot != null && snapshot.exists()) {
val recipeIds = snapshot.get(BOOKMARKS_FIELD) as? List<*>
trySend(recipeIds?.map { it.toString() } ?: emptyList())
}
}
awaitClose { listenerRegistration.remove() }
}
}

override suspend fun saveBookmark(recipeId: String) {
return suspendCancellableCoroutine { continuation ->
bookmarkRef.set(
mapOf(BOOKMARKS_FIELD to FieldValue.arrayUnion(recipeId)),
SetOptions.merge(),
)
.addOnSuccessListener {
continuation.resume(Unit)
}.addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
}
}

override suspend fun deleteBookmark(recipeId: String) {
return suspendCoroutine { continuation ->
bookmarkRef.update(BOOKMARKS_FIELD, FieldValue.arrayRemove(recipeId))
.addOnSuccessListener {
continuation.resume(Unit)
}.addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
}
}

override suspend fun isBookmarked(recipeId: String): Boolean {
return suspendCoroutine { continuation ->
bookmarkRef.get()
.addOnSuccessListener { document ->
val bookmarks = document.get(BOOKMARKS_FIELD) as List<*>?
continuation.resume(bookmarks?.contains(recipeId) ?: false)
}.addOnFailureListener { _ ->
continuation.resume(false)
}
}
}
}