From 9bec2165f3fbcf0cc2130239278761664f6a40c8 Mon Sep 17 00:00:00 2001 From: pesta Date: Sun, 3 Nov 2024 16:43:34 +0200 Subject: [PATCH 01/10] create BookmarkApi.kt to define the desired remote operations on Bookmarks. --- .../loki/plitso/data/remote/bookmark/BookmarkApi.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt new file mode 100644 index 0000000..f0f9b86 --- /dev/null +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt @@ -0,0 +1,12 @@ +package com.loki.plitso.data.remote.bookmark + +import com.loki.plitso.data.local.models.Bookmark + +interface BookmarkApi { + + fun saveBookmark(bookmark: Bookmark): Boolean + + fun deleteBookmark(recipeId: String) + + fun isBookmarked(recipeId: String): BookmarkApi +} From 43e9eec07671522eac19ff9fa775557102040e92 Mon Sep 17 00:00:00 2001 From: pesta Date: Sun, 3 Nov 2024 18:58:52 +0200 Subject: [PATCH 02/10] Edit the BookmarkApi. --- .../com/loki/plitso/data/remote/bookmark/BookmarkApi.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt index f0f9b86..afff71c 100644 --- a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt @@ -1,12 +1,15 @@ package com.loki.plitso.data.remote.bookmark import com.loki.plitso.data.local.models.Bookmark +import kotlinx.coroutines.flow.Flow interface BookmarkApi { - fun saveBookmark(bookmark: Bookmark): Boolean + val bookmarks: Flow> - fun deleteBookmark(recipeId: String) + suspend fun saveBookmark(recipeId: String): Boolean - fun isBookmarked(recipeId: String): BookmarkApi + suspend fun deleteBookmark(recipeId: String) + + suspend fun isBookmarked(recipeId: String): Boolean } From 71857188255319a5e9ff7625dcd65f2a4d8420ca Mon Sep 17 00:00:00 2001 From: pesta Date: Sun, 3 Nov 2024 18:59:47 +0200 Subject: [PATCH 03/10] Implement the BookmarkApi using Firebase firestore. --- .../data/remote/bookmark/BookmarkApiImpl.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt new file mode 100644 index 0000000..c00ff8d --- /dev/null +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt @@ -0,0 +1,70 @@ +package com.loki.plitso.data.remote.bookmark + +import com.google.firebase.Firebase +import com.google.firebase.firestore.FieldValue +import com.google.firebase.firestore.firestore +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val BOOKMARKS_FIELD = "bookmarks" + +class BookmarkApiImpl(userId: String): BookmarkApi { + + private val bookmarkRef = Firebase.firestore.document("users/$userId") + + override val bookmarks: Flow> + 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 ?: emptyList() + trySend(recipeIds) + } + } + awaitClose { listenerRegistration.remove() } + } + } + + + override suspend fun saveBookmark(recipeId: String): Boolean { + return suspendCoroutine {continuation -> + bookmarkRef.update(BOOKMARKS_FIELD, FieldValue.arrayUnion(recipeId)) + .addOnSuccessListener { + continuation.resume(true) + }.addOnFailureListener { + continuation.resume(false) + } + } + } + + 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)) + }.addOnFailureListener { _ -> + continuation.resume(false) + } + } + } +} From 8f38a9e11083e2eae803b1533bede261ad2eae93 Mon Sep 17 00:00:00 2001 From: pesta Date: Sun, 3 Nov 2024 20:45:29 +0200 Subject: [PATCH 04/10] Edit the BookmarkApi. --- .../plitso/data/remote/bookmark/BookmarkApi.kt | 2 +- .../data/remote/bookmark/BookmarkApiImpl.kt | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt index afff71c..c6b95be 100644 --- a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt @@ -7,7 +7,7 @@ interface BookmarkApi { val bookmarks: Flow> - suspend fun saveBookmark(recipeId: String): Boolean + suspend fun saveBookmark(recipeId: String) suspend fun deleteBookmark(recipeId: String) diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt index c00ff8d..1d6d742 100644 --- a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt @@ -2,15 +2,17 @@ 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 -private const val BOOKMARKS_FIELD = "bookmarks" +const val BOOKMARKS_FIELD = "bookmarks" class BookmarkApiImpl(userId: String): BookmarkApi { @@ -34,13 +36,13 @@ class BookmarkApiImpl(userId: String): BookmarkApi { } - override suspend fun saveBookmark(recipeId: String): Boolean { - return suspendCoroutine {continuation -> - bookmarkRef.update(BOOKMARKS_FIELD, FieldValue.arrayUnion(recipeId)) + override suspend fun saveBookmark(recipeId: String) { + return suspendCancellableCoroutine {continuation -> + bookmarkRef.set(mapOf(BOOKMARKS_FIELD to FieldValue.arrayUnion(recipeId)), SetOptions.merge()) .addOnSuccessListener { - continuation.resume(true) - }.addOnFailureListener { - continuation.resume(false) + continuation.resume(Unit) + }.addOnFailureListener { exception -> + continuation.resumeWithException(exception) } } } From 42a4d9124b764a4ebb5b93a4afb31753351f727c Mon Sep 17 00:00:00 2001 From: pesta Date: Sun, 3 Nov 2024 20:46:07 +0200 Subject: [PATCH 05/10] Create test cases for the add function of the BookmarkApi. --- .../remote/bookmark/BookmarkApiImplTest.kt | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt diff --git a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt new file mode 100644 index 0000000..8101143 --- /dev/null +++ b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt @@ -0,0 +1,97 @@ +package com.loki.plitso.data.remote.bookmark + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.firebase.Firebase +import com.google.firebase.firestore.SetOptions +import com.google.firebase.firestore.firestore +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine +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 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}) + } + + @After + fun cleanUp() = runBlocking{ + suspendCancellableCoroutine { continuation -> + Firebase.firestore.document("users/$user").delete().addOnCompleteListener { + continuation.resume(Unit) + } + + } + } +} From 0bd5116a3b370192ff8ffaedd0600dd7f2c802bb Mon Sep 17 00:00:00 2001 From: pesta Date: Sun, 3 Nov 2024 21:22:02 +0200 Subject: [PATCH 06/10] Create test cases for the delete function of the BookmarkApi. --- .../remote/bookmark/BookmarkApiImplTest.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt index 8101143..3094710 100644 --- a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt +++ b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt @@ -2,9 +2,11 @@ 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.SetOptions import com.google.firebase.firestore.firestore import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -20,6 +22,7 @@ class BookmarkApiImplTest{ private val user = "testUser" private val bookmarkApi = BookmarkApiImpl(user) + @Test fun addBookmarkNoExistingDocument() = runBlocking{ val expected = "recipe1" @@ -85,6 +88,54 @@ class BookmarkApiImplTest{ 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)) + } + @After fun cleanUp() = runBlocking{ suspendCancellableCoroutine { continuation -> From 05bdb8df8853bd1c4f0c4b7c60bcd180e15abf8d Mon Sep 17 00:00:00 2001 From: pesta Date: Sun, 3 Nov 2024 21:38:52 +0200 Subject: [PATCH 07/10] Create test cases for the getBookmarks function of the BookmarkApi. --- .../remote/bookmark/BookmarkApiImplTest.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt index 3094710..c465cb5 100644 --- a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt +++ b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt @@ -11,6 +11,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.time.withTimeoutOrNull +import kotlinx.coroutines.withTimeoutOrNull import org.junit.After import org.junit.Test import org.junit.runner.RunWith @@ -22,6 +24,59 @@ 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() + ) + ) + .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{ From 4100988f981cb7af08399b23c2d31ae56e5b0017 Mon Sep 17 00:00:00 2001 From: pesta Date: Mon, 4 Nov 2024 12:34:49 +0200 Subject: [PATCH 08/10] Create test cases for the isBookmarked function of the BookmarkApi. --- .../remote/bookmark/BookmarkApiImplTest.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt index c465cb5..2ce7f02 100644 --- a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt +++ b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt @@ -191,6 +191,62 @@ class BookmarkApiImplTest{ 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) + } + } + val bookmarks = bookmarkApi.bookmarks.first() + 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() + ) + ) + .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 -> From 0c13b300d6bb563be37d16048c2cd8967c297b6c Mon Sep 17 00:00:00 2001 From: pesta Date: Mon, 4 Nov 2024 12:36:30 +0200 Subject: [PATCH 09/10] fix bug in the isBookmarked function. Was throwing Exception if the current user is not in the database, now it returns false instead. --- .../com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt index 1d6d742..0fa8487 100644 --- a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt @@ -62,8 +62,8 @@ class BookmarkApiImpl(userId: String): BookmarkApi { return suspendCoroutine { continuation -> bookmarkRef.get() .addOnSuccessListener {document -> - val bookmarks = document.get(BOOKMARKS_FIELD) as List<*> - continuation.resume(bookmarks.contains(recipeId)) + val bookmarks = document.get(BOOKMARKS_FIELD) as List<*>? + continuation.resume(bookmarks?.contains(recipeId) ?: false) }.addOnFailureListener { _ -> continuation.resume(false) } From dd0e80cac58fd52ef1b055b7593dacb054f49ad8 Mon Sep 17 00:00:00 2001 From: pesta Date: Mon, 4 Nov 2024 15:34:39 +0200 Subject: [PATCH 10/10] fix some warnings for the ktlintFormat process. --- .../remote/bookmark/BookmarkApiImplTest.kt | 383 +++++++++--------- .../data/remote/bookmark/BookmarkApi.kt | 2 - .../data/remote/bookmark/BookmarkApiImpl.kt | 32 +- 3 files changed, 213 insertions(+), 204 deletions(-) diff --git a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt index 2ce7f02..0eb33aa 100644 --- a/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt +++ b/app/src/androidTest/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImplTest.kt @@ -3,15 +3,12 @@ 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.SetOptions import com.google.firebase.firestore.firestore import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.time.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull import org.junit.After import org.junit.Test @@ -19,241 +16,253 @@ import org.junit.runner.RunWith import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) -class BookmarkApiImplTest{ - +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) - } + 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) - } + 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) } - 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() + fun getBookmarkExistingDocumentWithEmptyBookmarkField() = + runBlocking { + suspendCancellableCoroutine { c -> + Firebase.firestore.document("users/$user") + .set( + mapOf( + "name" to "John Doe", + BOOKMARKS_FIELD to emptyList(), + ), ) - ) - .addOnCompleteListener { - c.resume(Unit) - } + .addOnCompleteListener { + c.resume(Unit) + } + } + val bookmarks = bookmarkApi.bookmarks.first() + assertEquals(0, bookmarks.size) } - 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) - } + 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) } - 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) - } + 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) - } + 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) } - 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") + 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) - } + .addOnCompleteListener { + c.resume(Unit) + } + } + val expected = "recipe1" + bookmarkApi.saveBookmark(expected) + val bookmarks = bookmarkApi.bookmarks.first() + assertEquals(2, bookmarks.size) + assert(bookmarks.contains(expected)) } - 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") + 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) - } + .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 }) } - 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) - } + 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", + fun deleteBookmarkFromExistingDocumentWithNonExistingBookmarkField() = + runBlocking { + suspendCancellableCoroutine { c -> + Firebase.firestore.document("users/$user") + .set( + mapOf( + "name" to "John Doe", + ), ) - ) - .addOnCompleteListener { - c.resume(Unit) - } + .addOnCompleteListener { + c.resume(Unit) + } + } + val recipeId = "recipe12" + bookmarkApi.deleteBookmark(recipeId) + val bookmarks = bookmarkApi.bookmarks.first() + assertEquals(0, bookmarks.size) } - 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") + 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) - } + .addOnCompleteListener { + c.resume(Unit) + } + } + val recipeId = "recipe12" + bookmarkApi.deleteBookmark(recipeId) + val bookmarks = bookmarkApi.bookmarks.first() + assertEquals(2, bookmarks.size) + assertFalse(bookmarks.contains(recipeId)) } - 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) - } + 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) - } + 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)) } - val bookmarks = bookmarkApi.bookmarks.first() - 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() + fun isBookmarkedExistingDocumentWithEmptyBookmarkField() = + runBlocking { + val recipeId = "recipe12" + suspendCancellableCoroutine { c -> + Firebase.firestore.document("users/$user") + .set( + mapOf( + "name" to "John Doe", + BOOKMARKS_FIELD to emptyList(), + ), ) - ) - .addOnCompleteListener { - c.resume(Unit) - } + .addOnCompleteListener { + c.resume(Unit) + } + } + assertEquals(false, bookmarkApi.isBookmarked(recipeId)) } - 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) - } + 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)) } - assertEquals(true, bookmarkApi.isBookmarked(recipeId)) - } @After - fun cleanUp() = runBlocking{ - suspendCancellableCoroutine { continuation -> - Firebase.firestore.document("users/$user").delete().addOnCompleteListener { - continuation.resume(Unit) + fun cleanUp() = + runBlocking { + suspendCancellableCoroutine { continuation -> + Firebase.firestore.document("users/$user").delete().addOnCompleteListener { + continuation.resume(Unit) + } } - } - } } diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt index c6b95be..ba12ce7 100644 --- a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApi.kt @@ -1,10 +1,8 @@ package com.loki.plitso.data.remote.bookmark -import com.loki.plitso.data.local.models.Bookmark import kotlinx.coroutines.flow.Flow interface BookmarkApi { - val bookmarks: Flow> suspend fun saveBookmark(recipeId: String) diff --git a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt index 0fa8487..359f22b 100644 --- a/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt +++ b/app/src/main/java/com/loki/plitso/data/remote/bookmark/BookmarkApiImpl.kt @@ -14,31 +14,33 @@ import kotlin.coroutines.suspendCoroutine const val BOOKMARKS_FIELD = "bookmarks" -class BookmarkApiImpl(userId: String): BookmarkApi { - +class BookmarkApiImpl(userId: String) : BookmarkApi { private val bookmarkRef = Firebase.firestore.document("users/$userId") override val bookmarks: Flow> 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 ?: emptyList() - trySend(recipeIds) + 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()) + return suspendCancellableCoroutine { continuation -> + bookmarkRef.set( + mapOf(BOOKMARKS_FIELD to FieldValue.arrayUnion(recipeId)), + SetOptions.merge(), + ) .addOnSuccessListener { continuation.resume(Unit) }.addOnFailureListener { exception -> @@ -61,7 +63,7 @@ class BookmarkApiImpl(userId: String): BookmarkApi { override suspend fun isBookmarked(recipeId: String): Boolean { return suspendCoroutine { continuation -> bookmarkRef.get() - .addOnSuccessListener {document -> + .addOnSuccessListener { document -> val bookmarks = document.get(BOOKMARKS_FIELD) as List<*>? continuation.resume(bookmarks?.contains(recipeId) ?: false) }.addOnFailureListener { _ ->