diff --git a/app/src/main/java/com/android/unio/model/association/Association.kt b/app/src/main/java/com/android/unio/model/association/Association.kt index 1e22f49ea..364f12348 100644 --- a/app/src/main/java/com/android/unio/model/association/Association.kt +++ b/app/src/main/java/com/android/unio/model/association/Association.kt @@ -1,10 +1,13 @@ package com.android.unio.model.association +import com.android.unio.model.firestore.FirestoreReferenceList +import com.android.unio.model.user.User + data class Association( val uid: String, val url: String = "", val acronym: String = "", val fullName: String = "", val description: String = "", - val members: List = emptyList() + val members: FirestoreReferenceList ) diff --git a/app/src/main/java/com/android/unio/model/association/AssociationRepository.kt b/app/src/main/java/com/android/unio/model/association/AssociationRepository.kt index cfe04a6ed..8fc7789a9 100644 --- a/app/src/main/java/com/android/unio/model/association/AssociationRepository.kt +++ b/app/src/main/java/com/android/unio/model/association/AssociationRepository.kt @@ -4,4 +4,10 @@ interface AssociationRepository { fun init(onSuccess: () -> Unit) fun getAssociations(onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit) + + fun getAssociationWithId( + id: String, + onSuccess: (Association) -> Unit, + onFailure: (Exception) -> Unit + ) } diff --git a/app/src/main/java/com/android/unio/model/association/AssociationRepositoryFirestore.kt b/app/src/main/java/com/android/unio/model/association/AssociationRepositoryFirestore.kt index 46c461c03..bf42d9d33 100644 --- a/app/src/main/java/com/android/unio/model/association/AssociationRepositoryFirestore.kt +++ b/app/src/main/java/com/android/unio/model/association/AssociationRepositoryFirestore.kt @@ -1,5 +1,9 @@ package com.android.unio.model.association +import com.android.unio.model.firestore.FirestorePaths.ASSOCIATION_PATH +import com.android.unio.model.firestore.FirestorePaths.USER_PATH +import com.android.unio.model.firestore.FirestoreReferenceList +import com.android.unio.model.user.UserRepositoryFirestore import com.google.firebase.Firebase import com.google.firebase.auth.auth import com.google.firebase.firestore.DocumentSnapshot @@ -33,18 +37,37 @@ class AssociationRepositoryFirestore(private val db: FirebaseFirestore) : Associ .addOnFailureListener { exception -> onFailure(exception) } } - fun hydrate(doc: DocumentSnapshot): Association { - return Association( - uid = doc.id, - url = doc.getString("url") ?: "", - acronym = doc.getString("acronym") ?: "", - fullName = doc.getString("fullName") ?: "", - description = doc.getString("description") ?: "", - members = doc.get("members") as? List ?: emptyList()) + override fun getAssociationWithId( + id: String, + onSuccess: (Association) -> Unit, + onFailure: (Exception) -> Unit + ) { + db.collection(ASSOCIATION_PATH) + .document(id) + .get() + .addOnSuccessListener { document -> + val association = hydrate(document) + onSuccess(association) + } + .addOnFailureListener { exception -> onFailure(exception) } } companion object { - private const val ASSOCIATION_PATH = "associations" - private const val USER_PATH = "users" + fun hydrate(doc: DocumentSnapshot): Association { + val memberUids = doc.get("members") as? List ?: emptyList() + val members = + FirestoreReferenceList.fromList( + list = memberUids, + collectionPath = USER_PATH, + hydrate = UserRepositoryFirestore::hydrate) + + return Association( + uid = doc.id, + url = doc.getString("url") ?: "", + acronym = doc.getString("acronym") ?: "", + fullName = doc.getString("fullName") ?: "", + description = doc.getString("description") ?: "", + members = members) + } } } diff --git a/app/src/main/java/com/android/unio/model/association/MockAssociations.kt b/app/src/main/java/com/android/unio/model/association/MockAssociations.kt index 24bd55022..7573fbb8c 100644 --- a/app/src/main/java/com/android/unio/model/association/MockAssociations.kt +++ b/app/src/main/java/com/android/unio/model/association/MockAssociations.kt @@ -1,5 +1,9 @@ package com.android.unio.model.association +import com.android.unio.model.firestore.FirestorePaths.USER_PATH +import com.android.unio.model.firestore.FirestoreReferenceList +import com.android.unio.model.user.UserRepositoryFirestore + enum class AssociationType { MUSIC, FESTIVALS, @@ -12,6 +16,11 @@ enum class AssociationType { data class MockAssociation(val association: Association, val type: AssociationType) +val emptyMembers = { + FirestoreReferenceList.empty( + collectionPath = USER_PATH, hydrate = UserRepositoryFirestore::hydrate) +} + val mockAssociations = listOf( MockAssociation( @@ -21,7 +30,7 @@ val mockAssociations = fullName = "Musical Association", description = "AGEPoly Commission – stimulation of the practice of music on the campus", - members = emptyList()), + members = emptyMembers()), AssociationType.MUSIC), MockAssociation( Association( @@ -30,7 +39,7 @@ val mockAssociations = fullName = "Nuit De la Magistrale Association", description = "AGEPoly Commission – party following the formal Magistrale Graduation Ceremony", - members = emptyList()), + members = emptyMembers()), AssociationType.FESTIVALS), MockAssociation( Association( @@ -38,7 +47,7 @@ val mockAssociations = acronym = "Balélec", fullName = "Festival Balélec", description = "Open-air unique en Suisse, organisée par des bénévoles étudiants.", - members = emptyList()), + members = emptyMembers()), AssociationType.FESTIVALS), MockAssociation( Association( @@ -46,7 +55,7 @@ val mockAssociations = acronym = "Artiphys", fullName = "Festival Artiphys", description = "Festival à l'EPFL", - members = emptyList()), + members = emptyMembers()), AssociationType.FESTIVALS), MockAssociation( Association( @@ -54,7 +63,7 @@ val mockAssociations = acronym = "Sysmic", fullName = "Festival Sysmic", description = "Festival à l'EPFL", - members = emptyList()), + members = emptyMembers()), AssociationType.FESTIVALS), MockAssociation( Association( @@ -62,7 +71,7 @@ val mockAssociations = acronym = "IFL", fullName = "Innovation Forum Lausanne", description = "Innovation Forum Lausanne", - members = emptyList()), + members = emptyMembers()), AssociationType.INNOVATION), MockAssociation( Association( @@ -70,6 +79,6 @@ val mockAssociations = acronym = "Clic", fullName = "Clic Association", description = "Association of EPFL Students of IC Faculty", - members = emptyList()), + members = emptyMembers()), AssociationType.FACULTIES), ) diff --git a/app/src/main/java/com/android/unio/model/firestore/FirestorePaths.kt b/app/src/main/java/com/android/unio/model/firestore/FirestorePaths.kt new file mode 100644 index 000000000..549fae7e7 --- /dev/null +++ b/app/src/main/java/com/android/unio/model/firestore/FirestorePaths.kt @@ -0,0 +1,6 @@ +package com.android.unio.model.firestore + +object FirestorePaths { + const val ASSOCIATION_PATH = "associations" + const val USER_PATH = "users" +} diff --git a/app/src/main/java/com/android/unio/model/firestore/FirestoreReferenceList.kt b/app/src/main/java/com/android/unio/model/firestore/FirestoreReferenceList.kt new file mode 100644 index 000000000..f24e22466 --- /dev/null +++ b/app/src/main/java/com/android/unio/model/firestore/FirestoreReferenceList.kt @@ -0,0 +1,91 @@ +package com.android.unio.model.firestore + +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * A class that represents a list of Firestore objects that are identified by their UIDs. The list + * is stored as a [StateFlow] and can be updated by calling [requestAll]. + * + * In a @Composable function, the list should be accessed using: + * ```kotlin + * val list by FirestoreReferenceList.list.collectAsState() + * ``` + * + * @param T The type of the objects in the list. + * @property db The [FirebaseFirestore] instance to use. + * @property collectionPath The path to the Firestore collection that contains the objects. + * @property hydrate A function that converts a [DocumentSnapshot] to a [T]. + */ +class FirestoreReferenceList( + private val db: FirebaseFirestore, + private val collectionPath: String, + private val hydrate: (DocumentSnapshot) -> T +) : ReferenceList { + // The internal list of UIDs. + private var _uids = mutableListOf() + + // The internal list of objects. + private val _list = MutableStateFlow>(emptyList()) + + // The public list of objects. + val list: StateFlow> = _list + + /** + * Adds a UID to the list. + * + * @param uid The UID to add. + */ + override fun add(uid: String) { + _uids.add(uid) + } + + /** + * Adds a list of UIDs to the list. + * + * @param uids The UIDs to add. + */ + override fun addAll(uids: List) { + _uids.addAll(uids) + } + + /** Requests all documents from Firestore and updates the list. */ + override fun requestAll() { + println("Requesting all") + _list.value = emptyList() + _uids.forEach { uid -> + db.collection(collectionPath).document(uid).get().addOnSuccessListener { result -> + val item = hydrate(result) + _list.value += item + println("Added $item") + } + } + } + + companion object { + /** Creates a [FirestoreReferenceList] from a list of UIDs. */ + fun fromList( + list: List, + db: FirebaseFirestore = Firebase.firestore, + collectionPath: String, + hydrate: (DocumentSnapshot) -> T + ): FirestoreReferenceList { + val result = FirestoreReferenceList(db, collectionPath, hydrate) + result.addAll(list) + return result + } + + /** Creates an empty [FirestoreReferenceList]. */ + fun empty( + db: FirebaseFirestore = Firebase.firestore, + collectionPath: String, + hydrate: (DocumentSnapshot) -> T + ): FirestoreReferenceList { + return FirestoreReferenceList(db, collectionPath, hydrate) + } + } +} diff --git a/app/src/main/java/com/android/unio/model/firestore/ReferenceList.kt b/app/src/main/java/com/android/unio/model/firestore/ReferenceList.kt new file mode 100644 index 000000000..e4c3ccea6 --- /dev/null +++ b/app/src/main/java/com/android/unio/model/firestore/ReferenceList.kt @@ -0,0 +1,9 @@ +package com.android.unio.model.firestore + +interface ReferenceList { + fun add(uid: String) + + fun addAll(uids: List) + + fun requestAll() +} diff --git a/app/src/main/java/com/android/unio/model/user/User.kt b/app/src/main/java/com/android/unio/model/user/User.kt index 84d5706ea..666ccebb1 100644 --- a/app/src/main/java/com/android/unio/model/user/User.kt +++ b/app/src/main/java/com/android/unio/model/user/User.kt @@ -1,10 +1,11 @@ package com.android.unio.model.user import com.android.unio.model.association.Association +import com.android.unio.model.firestore.FirestoreReferenceList data class User( - val id: String, + val uid: String, val name: String, val email: String, - val followingAssociations: List + val followingAssociations: FirestoreReferenceList ) diff --git a/app/src/main/java/com/android/unio/model/user/UserRepository.kt b/app/src/main/java/com/android/unio/model/user/UserRepository.kt new file mode 100644 index 000000000..37d7e4dd9 --- /dev/null +++ b/app/src/main/java/com/android/unio/model/user/UserRepository.kt @@ -0,0 +1,9 @@ +package com.android.unio.model.user + +interface UserRepository { + fun init(onSuccess: () -> Unit) + + fun getUsers(onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit) + + fun getUserWithId(id: String, onSuccess: (User) -> Unit, onFailure: (Exception) -> Unit) +} diff --git a/app/src/main/java/com/android/unio/model/user/UserRepositoryFirestore.kt b/app/src/main/java/com/android/unio/model/user/UserRepositoryFirestore.kt new file mode 100644 index 000000000..7ca200251 --- /dev/null +++ b/app/src/main/java/com/android/unio/model/user/UserRepositoryFirestore.kt @@ -0,0 +1,67 @@ +package com.android.unio.model.user + +import com.android.unio.model.association.AssociationRepositoryFirestore +import com.android.unio.model.firestore.FirestorePaths.ASSOCIATION_PATH +import com.android.unio.model.firestore.FirestorePaths.USER_PATH +import com.android.unio.model.firestore.FirestoreReferenceList +import com.google.firebase.Firebase +import com.google.firebase.auth.auth +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore + +class UserRepositoryFirestore(private val db: FirebaseFirestore) : UserRepository { + + override fun init(onSuccess: () -> Unit) { + Firebase.auth.addAuthStateListener { + if (it.currentUser != null) { + onSuccess() + } + } + } + + override fun getUsers(onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit) { + db.collection(USER_PATH) + .get() + .addOnSuccessListener { result -> + val associations = result.map { hydrate(it) } + onSuccess(associations) + } + .addOnFailureListener { exception -> onFailure(exception) } + } + + override fun getUserWithId( + id: String, + onSuccess: (User) -> Unit, + onFailure: (Exception) -> Unit + ) { + db.collection(USER_PATH) + .document(id) + .get() + .addOnSuccessListener { document -> + val association = hydrate(document) + onSuccess(association) + } + .addOnFailureListener { exception -> onFailure(exception) } + } + + companion object { + fun hydrate(doc: DocumentSnapshot): User { + val db = FirebaseFirestore.getInstance() + + val followingAssociationsUids = + doc.get("followingAssociations") as? List ?: emptyList() + val followingAssociations = + FirestoreReferenceList.fromList( + followingAssociationsUids, + db, + ASSOCIATION_PATH, + AssociationRepositoryFirestore::hydrate) + + return User( + uid = doc.id, + name = doc.getString("name") ?: "", + email = doc.getString("email") ?: "", + followingAssociations = followingAssociations) + } + } +} diff --git a/app/src/test/java/com/android/unio/model/association/AssociationRepositoryFirestoreTest.kt b/app/src/test/java/com/android/unio/model/association/AssociationRepositoryFirestoreTest.kt index ff9bdfe2e..74cd80bf0 100644 --- a/app/src/test/java/com/android/unio/model/association/AssociationRepositoryFirestoreTest.kt +++ b/app/src/test/java/com/android/unio/model/association/AssociationRepositoryFirestoreTest.kt @@ -1,65 +1,98 @@ package com.android.unio.model.association +import androidx.test.core.app.ApplicationProvider +import com.android.unio.model.firestore.FirestorePaths.USER_PATH +import com.android.unio.model.firestore.FirestoreReferenceList +import com.android.unio.model.user.UserRepositoryFirestore import com.google.android.gms.tasks.OnSuccessListener import com.google.android.gms.tasks.Task +import com.google.firebase.FirebaseApp import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.QueryDocumentSnapshot import com.google.firebase.firestore.QuerySnapshot import junit.framework.TestCase.assertEquals import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.eq +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class AssociationRepositoryFirestoreTest { @Mock private lateinit var db: FirebaseFirestore @Mock private lateinit var collectionReference: CollectionReference @Mock private lateinit var querySnapshot: QuerySnapshot @Mock private lateinit var queryDocumentSnapshot1: QueryDocumentSnapshot @Mock private lateinit var queryDocumentSnapshot2: QueryDocumentSnapshot - @Mock private lateinit var task: Task + @Mock private lateinit var documentReference: DocumentReference + @Mock private lateinit var querySnapshotTask: Task + @Mock private lateinit var documentSnapshotTask: Task private lateinit var repository: AssociationRepositoryFirestore - private val association1 = - Association( - uid = "1", - acronym = "ACM", - fullName = "Association for Computing Machinery", - description = "ACM is the world's largest educational and scientific computing society.", - members = mutableListOf("1", "2")) - - private val association2 = - Association( - uid = "2", - acronym = "IEEE", - fullName = "Institute of Electrical and Electronics Engineers", - description = - "IEEE is the world's largest technical professional organization dedicated to advancing technology for the benefit of humanity.", - members = mutableListOf("3", "4")) + private lateinit var association1: Association + private lateinit var association2: Association @Before fun setUp() { MockitoAnnotations.openMocks(this) + // Initialize Firebase if necessary + if (FirebaseApp.getApps(ApplicationProvider.getApplicationContext()).isEmpty()) { + FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext()) + } + + association1 = + Association( + uid = "1", + acronym = "ACM", + fullName = "Association for Computing Machinery", + description = + "ACM is the world's largest educational and scientific computing society.", + members = + FirestoreReferenceList.fromList( + listOf("1", "2"), db, USER_PATH, UserRepositoryFirestore::hydrate)) + + association2 = + Association( + uid = "2", + acronym = "IEEE", + fullName = "Institute of Electrical and Electronics Engineers", + description = + "IEEE is the world's largest technical professional organization dedicated to advancing technology for the benefit of humanity.", + members = + FirestoreReferenceList.fromList( + listOf("3", "4"), db, USER_PATH, UserRepositoryFirestore::hydrate)) + // When getting the collection, return the task `when`(db.collection(eq("associations"))).thenReturn(collectionReference) - `when`(collectionReference.get()).thenReturn(task) + `when`(collectionReference.get()).thenReturn(querySnapshotTask) + `when`(collectionReference.document(eq(association1.uid))).thenReturn(documentReference) + `when`(documentReference.get()).thenReturn(documentSnapshotTask) + + // When the query snapshot is iterated, return the two query document snapshots + `when`(querySnapshot.iterator()) + .thenReturn(mutableListOf(queryDocumentSnapshot1, queryDocumentSnapshot2).iterator()) // When the task is successful, return the query snapshot - `when`(task.addOnSuccessListener(any())).thenAnswer { invocation -> + `when`(querySnapshotTask.addOnSuccessListener(any())).thenAnswer { invocation -> val callback = invocation.arguments[0] as OnSuccessListener callback.onSuccess(querySnapshot) - task + querySnapshotTask } - // When the query snapshot is iterated, return the two query document snapshots - `when`(querySnapshot.iterator()) - .thenReturn(mutableListOf(queryDocumentSnapshot1, queryDocumentSnapshot2).iterator()) + `when`(documentSnapshotTask.addOnSuccessListener(any())).thenAnswer { invocation -> + val callback = invocation.arguments[0] as OnSuccessListener + callback.onSuccess(queryDocumentSnapshot1) + documentSnapshotTask + } // When the query document snapshots are queried for specific fields, return the fields `when`(queryDocumentSnapshot1.id).thenReturn(association1.uid) @@ -73,6 +106,7 @@ class AssociationRepositoryFirestoreTest { @Test fun testGetAssociations() { + `when`(queryDocumentSnapshot2.id).thenReturn(association2.uid) `when`(queryDocumentSnapshot2.getString("acronym")).thenReturn(association2.acronym) `when`(queryDocumentSnapshot2.getString("fullName")).thenReturn(association2.fullName) @@ -82,8 +116,15 @@ class AssociationRepositoryFirestoreTest { repository.getAssociations( onSuccess = { associations -> assertEquals(2, associations.size) - assertEquals(association1, associations[0]) - assertEquals(association2, associations[1]) + assertEquals(association1.uid, associations[0].uid) + assertEquals(association1.acronym, associations[0].acronym) + assertEquals(association1.fullName, associations[0].fullName) + assertEquals(association1.description, associations[0].description) + + assertEquals(association2.uid, associations[1].uid) + assertEquals(association2.acronym, associations[1].acronym) + assertEquals(association2.fullName, associations[1].fullName) + assertEquals(association2.description, associations[1].description) }, onFailure = { exception -> assert(false) }) } @@ -95,16 +136,35 @@ class AssociationRepositoryFirestoreTest { repository.getAssociations( onSuccess = { associations -> - assertEquals(2, associations.size) - assertEquals(association1, associations[0]) - assertEquals( + val emptyAssociation = Association( uid = association2.uid, - acronym = "", - fullName = "", - description = "", - members = emptyList()), - associations[1]) + members = FirestoreReferenceList.empty(db, "", UserRepositoryFirestore::hydrate)) + + assertEquals(2, associations.size) + + assertEquals(association1.uid, associations[0].uid) + assertEquals(association1.acronym, associations[0].acronym) + assertEquals(association1.fullName, associations[0].fullName) + assertEquals(association1.description, associations[0].description) + + assertEquals(emptyAssociation.uid, associations[1].uid) + assertEquals("", associations[1].acronym) + assertEquals("", associations[1].fullName) + assertEquals("", associations[1].description) + }, + onFailure = { exception -> assert(false) }) + } + + @Test + fun testGetAssociationWithId() { + repository.getAssociationWithId( + association1.uid, + onSuccess = { association -> + assertEquals(association1.uid, association.uid) + assertEquals(association1.acronym, association.acronym) + assertEquals(association1.fullName, association.fullName) + assertEquals(association1.description, association.description) }, onFailure = { exception -> assert(false) }) } diff --git a/app/src/test/java/com/android/unio/model/association/ExploreViewModelTest.kt b/app/src/test/java/com/android/unio/model/association/ExploreViewModelTest.kt index 90e6e56a0..21aded2ac 100644 --- a/app/src/test/java/com/android/unio/model/association/ExploreViewModelTest.kt +++ b/app/src/test/java/com/android/unio/model/association/ExploreViewModelTest.kt @@ -1,5 +1,9 @@ package com.android.unio.model.association +import com.android.unio.model.firestore.FirestorePaths.USER_PATH +import com.android.unio.model.firestore.FirestoreReferenceList +import com.android.unio.model.user.UserRepositoryFirestore +import com.google.firebase.firestore.FirebaseFirestore import junit.framework.TestCase.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,31 +23,39 @@ import org.mockito.kotlin.any class ExploreViewModelTest { @Mock private lateinit var repository: AssociationRepositoryFirestore + @Mock private lateinit var db: FirebaseFirestore + private lateinit var viewModel: ExploreViewModel @OptIn(ExperimentalCoroutinesApi::class) private val testDispatcher = UnconfinedTestDispatcher() - private val testAssociations = - listOf( - Association( - uid = "1", - acronym = "ACM", - fullName = "Association for Computing Machinery", - description = - "ACM is the world's largest educational and scientific computing society.", - members = listOf("1", "2")), - Association( - uid = "2", - acronym = "IEEE", - fullName = "Institute of Electrical and Electronics Engineers", - description = "IEEE is the world's largest technical professional organization.", - members = listOf("3", "4"))) + private lateinit var testAssociations: List @Before fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(testDispatcher) + testAssociations = + listOf( + Association( + uid = "1", + acronym = "ACM", + fullName = "Association for Computing Machinery", + description = + "ACM is the world's largest educational and scientific computing society.", + members = + FirestoreReferenceList.fromList( + listOf("1", "2"), db, USER_PATH, UserRepositoryFirestore::hydrate)), + Association( + uid = "2", + acronym = "IEEE", + fullName = "Institute of Electrical and Electronics Engineers", + description = "IEEE is the world's largest technical professional organization.", + members = + FirestoreReferenceList.fromList( + listOf("3", "4"), db, USER_PATH, UserRepositoryFirestore::hydrate))) + viewModel = ExploreViewModel(repository) } diff --git a/app/src/test/java/com/android/unio/model/firestore/FirestoreReferenceListTest.kt b/app/src/test/java/com/android/unio/model/firestore/FirestoreReferenceListTest.kt new file mode 100644 index 000000000..0c4673bc9 --- /dev/null +++ b/app/src/test/java/com/android/unio/model/firestore/FirestoreReferenceListTest.kt @@ -0,0 +1,97 @@ +package com.android.unio.model.firestore + +import com.google.android.gms.tasks.OnSuccessListener +import com.google.android.gms.tasks.Task +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.timeout +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class FirestoreReferenceListTest { + + @Mock private lateinit var mockFirestore: FirebaseFirestore + @Mock private lateinit var mockCollectionRef: CollectionReference + @Mock private lateinit var mockDocumentRef: DocumentReference + @Mock private lateinit var mockSnapshot: DocumentSnapshot + @Mock private lateinit var mockTask: Task + @Mock private lateinit var firestoreReferenceList: FirestoreReferenceList + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + + whenever(mockFirestore.collection(any())).thenReturn(mockCollectionRef) + whenever(mockCollectionRef.document(any())).thenReturn(mockDocumentRef) + whenever(mockDocumentRef.get()).thenReturn(mockTask) + whenever(mockTask.addOnSuccessListener(any())).thenAnswer { invocation -> + val thread = Thread { + Thread.sleep(100) + val callback = invocation.arguments[0] as OnSuccessListener + callback.onSuccess(mockSnapshot) + } + thread.start() + mockTask + } + + firestoreReferenceList = + FirestoreReferenceList(mockFirestore, "testPath") { snapshot -> + snapshot.getString("data") ?: "" + } + } + + @Test + fun `test requestAll fetches documents and updates list`() = runTest { + whenever(mockSnapshot.getString("data")).thenReturn("Item1", "Item2") + + // Add UIDs and call requestAll + firestoreReferenceList.addAll(listOf("uid1", "uid2")) + firestoreReferenceList.requestAll() + + // Verify firestore calls after 200ms + verify(mockFirestore, timeout(200).times(2)).collection("testPath") + verify(mockCollectionRef, timeout(200).times(2)).document(any()) + verify(mockDocumentRef, timeout(200).times(2)).get() + } + + @Test + fun `test requestAll clears list before updating`() = runTest { + // Set initial state + firestoreReferenceList.addAll(listOf("uid1", "uid2")) + firestoreReferenceList.requestAll() + + // Verify the list is cleared + assertEquals(0, firestoreReferenceList.list.value.size) + } + + @Test + fun `test fromList creates FirestoreReferenceList with UIDs`() = runTest { + val list = listOf("uid1", "uid2") + val fromList = + FirestoreReferenceList.fromList(list, mockFirestore, "testPath") { snapshot -> + snapshot.getString("data") ?: "" + } + + assertEquals(emptyList(), fromList.list.first()) + } + + @Test + fun `test empty creates FirestoreReferenceList without UIDs`() = runTest { + val emptyList = + FirestoreReferenceList.empty(mockFirestore, "testPath") { snapshot -> + snapshot.getString("data") ?: "" + } + + assertEquals(emptyList(), emptyList.list.first()) + } +} diff --git a/app/src/test/java/com/android/unio/model/user/UserRepositoryFirestoreTest.kt b/app/src/test/java/com/android/unio/model/user/UserRepositoryFirestoreTest.kt new file mode 100644 index 000000000..4e83dac30 --- /dev/null +++ b/app/src/test/java/com/android/unio/model/user/UserRepositoryFirestoreTest.kt @@ -0,0 +1,162 @@ +package com.android.unio.model.user + +import androidx.test.core.app.ApplicationProvider +import com.android.unio.model.association.AssociationRepositoryFirestore +import com.android.unio.model.firestore.FirestoreReferenceList +import com.google.android.gms.tasks.OnSuccessListener +import com.google.android.gms.tasks.Task +import com.google.firebase.FirebaseApp +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.QueryDocumentSnapshot +import com.google.firebase.firestore.QuerySnapshot +import junit.framework.TestCase.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UserRepositoryFirestoreTest { + @Mock private lateinit var db: FirebaseFirestore + @Mock private lateinit var collectionReference: CollectionReference + @Mock private lateinit var querySnapshot: QuerySnapshot + @Mock private lateinit var queryDocumentSnapshot1: QueryDocumentSnapshot + @Mock private lateinit var queryDocumentSnapshot2: QueryDocumentSnapshot + @Mock private lateinit var documentReference: DocumentReference + @Mock private lateinit var querySnapshotTask: Task + @Mock private lateinit var documentSnapshotTask: Task + + private lateinit var repository: UserRepositoryFirestore + + private lateinit var user1: User + private lateinit var user2: User + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + // Initialize Firebase if necessary + if (FirebaseApp.getApps(ApplicationProvider.getApplicationContext()).isEmpty()) { + FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext()) + } + + user1 = + User( + uid = "1", + email = "example1@abcd.com", + name = "Example 1", + followingAssociations = + FirestoreReferenceList.empty(db, "", AssociationRepositoryFirestore::hydrate)) + + user2 = + User( + uid = "2", + email = "example2@abcd.com", + name = "Example 2", + followingAssociations = + FirestoreReferenceList.empty(db, "", AssociationRepositoryFirestore::hydrate)) + + // When getting the collection, return the task + `when`(db.collection(eq("users"))).thenReturn(collectionReference) + `when`(collectionReference.get()).thenReturn(querySnapshotTask) + `when`(collectionReference.document(eq(user1.uid))).thenReturn(documentReference) + `when`(documentReference.get()).thenReturn(documentSnapshotTask) + + // When the query snapshot is iterated, return the two query document snapshots + `when`(querySnapshot.iterator()) + .thenReturn(mutableListOf(queryDocumentSnapshot1, queryDocumentSnapshot2).iterator()) + + // When the task is successful, return the query snapshot + `when`(querySnapshotTask.addOnSuccessListener(any())).thenAnswer { invocation -> + val callback = invocation.arguments[0] as OnSuccessListener + callback.onSuccess(querySnapshot) + querySnapshotTask + } + + `when`(documentSnapshotTask.addOnSuccessListener(any())).thenAnswer { invocation -> + val callback = invocation.arguments[0] as OnSuccessListener + callback.onSuccess(queryDocumentSnapshot1) + documentSnapshotTask + } + + // When the query document snapshots are queried for specific fields, return the fields + `when`(queryDocumentSnapshot1.id).thenReturn(user1.uid) + `when`(queryDocumentSnapshot1.getString("name")).thenReturn(user1.name) + `when`(queryDocumentSnapshot1.getString("email")).thenReturn(user1.email) + `when`(queryDocumentSnapshot1.get("followingAssociations")) + .thenReturn(user1.followingAssociations) + + repository = UserRepositoryFirestore(db) + } + + @Test + fun testGetUsers() { + + `when`(queryDocumentSnapshot2.id).thenReturn(user2.uid) + `when`(queryDocumentSnapshot2.getString("name")).thenReturn(user2.name) + `when`(queryDocumentSnapshot2.getString("email")).thenReturn(user2.email) + `when`(queryDocumentSnapshot2.get("followingAssociations")) + .thenReturn(user2.followingAssociations) + + repository.getUsers( + onSuccess = { users -> + assertEquals(2, users.size) + + assertEquals(user1.uid, users[0].uid) + assertEquals(user1.name, users[0].name) + assertEquals(user1.email, users[0].email) + + assertEquals(user2.uid, users[1].uid) + assertEquals(user2.name, users[1].name) + assertEquals(user2.email, users[1].email) + }, + onFailure = { exception -> assert(false) }) + } + + @Test + fun testGetAssociationsWithMissingFields() { + // Only set the ID for the second association, leaving the other fields as null + `when`(queryDocumentSnapshot2.id).thenReturn(user2.uid) + + repository.getUsers( + onSuccess = { users -> + val emptyUser = + User( + uid = user2.uid, + email = "", + name = "", + followingAssociations = + FirestoreReferenceList.empty(db, "", AssociationRepositoryFirestore::hydrate)) + assertEquals(2, users.size) + + assertEquals(user1.uid, users[0].uid) + assertEquals(user1.name, users[0].name) + assertEquals(user1.email, users[0].email) + + assertEquals(emptyUser.uid, users[1].uid) + assertEquals("", users[1].name) + assertEquals("", users[1].email) + }, + onFailure = { exception -> assert(false) }) + } + + @Test + fun testGetUserWithId() { + repository.getUserWithId( + id = user1.uid, + onSuccess = { user -> + assertEquals(user1.uid, user.uid) + assertEquals(user1.name, user.name) + assertEquals(user1.email, user.email) + }, + onFailure = { exception -> assert(false) }) + } +} diff --git a/app/src/test/java/com/android/unio/model/user/UserTest.kt b/app/src/test/java/com/android/unio/model/user/UserTest.kt index c60c5df73..9d75bb35b 100644 --- a/app/src/test/java/com/android/unio/model/user/UserTest.kt +++ b/app/src/test/java/com/android/unio/model/user/UserTest.kt @@ -1,16 +1,33 @@ package com.android.unio.model.user -import com.android.unio.model.association.Association +import com.android.unio.model.association.AssociationRepositoryFirestore +import com.android.unio.model.firestore.FirestoreReferenceList +import com.google.firebase.firestore.FirebaseFirestore import junit.framework.TestCase.assertEquals +import org.junit.Before import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations class UserTest { + @Mock private lateinit var db: FirebaseFirestore + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + } + @Test fun testUser() { - val user = User("1", "John", "john@example.com", emptyList()) - assertEquals("1", user.id) + val user = + User( + "1", + "John", + "john@example.com", + FirestoreReferenceList.empty( + db, "associations", AssociationRepositoryFirestore::hydrate)) + assertEquals("1", user.uid) assertEquals("John", user.name) assertEquals("john@example.com", user.email) - assertEquals(emptyList(), user.followingAssociations) } }