From 3be56d09fefca9a9358a9391095e772911be3f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Tue, 14 May 2024 00:17:23 +0200 Subject: [PATCH 1/8] Persist profile on Firebase --- .../model/remote/ProfileConnection.kt | 9 ++--- .../ui/screens/EditProfileScreen.kt | 3 ++ .../wanderwave/ui/screens/ProfileScreen.kt | 1 + .../wanderwave/viewmodel/ProfileViewModel.kt | 35 +++++++++++++------ 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt index e862eaa83..cdc4d0d73 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt @@ -26,14 +26,11 @@ class ProfileConnection( override val db = database private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - override fun isUidExisting(spotifyUid: String, callback: (Boolean, Profile?) -> Unit) { + override fun isUidExisting(firebaseUid: String, callback: (Boolean, Profile?) -> Unit) { db.collection("users") - .whereEqualTo("spotifyUid", spotifyUid) + .document(firebaseUid) .get() - .addOnSuccessListener { documents -> - val isExisting = documents.size() > 0 - callback(isExisting, if (isExisting) documentToItem(documents.documents[0]) else null) - } + .addOnSuccessListener { document -> callback(true, documentToItem(document)) } .addOnFailureListener { exception -> Log.w("Firestore", "Error getting documents: ", exception) callback(false, null) // Assuming failure means document doesn't exist diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/EditProfileScreen.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/EditProfileScreen.kt index 1ec3ebfd0..4d5b3bc1e 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/EditProfileScreen.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/EditProfileScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -50,6 +51,8 @@ fun EditProfileScreen(navActions: NavigationActions, viewModel: ProfileViewModel var lastName by remember { mutableStateOf(profile2.lastName) } var description by remember { mutableStateOf(profile2.description) } + LaunchedEffect(Unit) { viewModel.getProfileOfCurrentUser(true) } + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.testTag("editProfileScreen")) { diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileScreen.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileScreen.kt index 7150b09d1..babeddd85 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileScreen.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileScreen.kt @@ -70,6 +70,7 @@ fun ProfileScreen(navActions: NavigationActions, viewModel: ProfileViewModel) { val currentProfile: Profile = currentProfileState ?: return LaunchedEffect(Unit) { + viewModel.getProfileOfCurrentUser(true) viewModel.createSpecificSongList(ListType.TOP_SONGS) viewModel.createSpecificSongList(ListType.CHOSEN_SONGS) } diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt index 22005bad2..51cb8b845 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt @@ -1,7 +1,9 @@ package ch.epfl.cs311.wanderwave.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController import ch.epfl.cs311.wanderwave.model.data.ListType import ch.epfl.cs311.wanderwave.model.data.Profile import ch.epfl.cs311.wanderwave.model.data.Track @@ -16,7 +18,6 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch // Define a simple class for a song list data class SongList(val name: ListType, val tracks: List = mutableListOf()) @@ -26,19 +27,20 @@ class ProfileViewModel @Inject constructor( private val repository: ProfileRepository, // TODO revoir - private val spotifyController: SpotifyController + private val spotifyController: SpotifyController, + private val authenticationController: AuthenticationController ) : ViewModel(), SpotifySongsActions { private val _profile = MutableStateFlow( Profile( - firstName = "My FirstName", - lastName = "My LastName", - description = "My Description", + firstName = "...", + lastName = "...", + description = "...", numberOfLikes = 0, isPublic = true, - spotifyUid = "My Spotify UID", - firebaseUid = "My Firebase UID", + spotifyUid = "", + firebaseUid = "", profilePictureUri = null)) val profile: StateFlow = _profile @@ -99,13 +101,26 @@ constructor( _isInPublicMode.value = !_isInPublicMode.value } - fun getProfileByID(id: String) { - viewModelScope.launch { - repository.getItem(id).collect { fetchedProfile -> + suspend fun getProfileByID(id: String, create: Boolean = false) { + repository.isUidExisting(id) { exists, fetchedProfile -> + if (exists) { + _profile.value = fetchedProfile!! _uiState.value = UIState(profile = fetchedProfile, isLoading = false) + } else if (create) { + val newProfile = profile.value.copy(firebaseUid = id) + _profile.value = newProfile + repository.addItemWithId(newProfile) + _uiState.value = UIState(profile = newProfile, isLoading = false) + } else { + Log.e("ProfileViewModel", "Profile not found") } } } + + suspend fun getProfileOfCurrentUser(create: Boolean = false) { + val currentUserId = authenticationController.getUserData()!!.id + getProfileByID(currentUserId, create) + } /** * Get all the element of the main screen and add them to the top list * From 24d384f886b96fafec581b768d643b8a080bc156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Tue, 14 May 2024 00:38:15 +0200 Subject: [PATCH 2/8] Fix tests --- .../wanderwave/model/ProfileConnectionTest.kt | 35 ++++++++++--------- .../cs311/wanderwave/ui/EditProfileTest.kt | 5 ++- .../epfl/cs311/wanderwave/ui/ProfileTest.kt | 5 ++- .../viewmodel/ProfileViewModelTest.kt | 5 ++- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt index 7c390f23d..69d3c86b2 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt @@ -5,13 +5,13 @@ import ch.epfl.cs311.wanderwave.model.data.Profile import ch.epfl.cs311.wanderwave.model.data.Track import ch.epfl.cs311.wanderwave.model.remote.ProfileConnection import ch.epfl.cs311.wanderwave.model.remote.TrackConnection +import com.google.android.gms.tasks.OnFailureListener 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 com.google.firebase.firestore.Query import com.google.firebase.firestore.QuerySnapshot import io.mockk.MockKAnnotations import io.mockk.coVerify @@ -290,10 +290,8 @@ public class ProfileConnectionTest { // Pass the mock Firestore instance to your BeaconConnection val collectionReference = mockk() - val query = mockk() - val mockTask = mockk>() - val mockQuerySnapshot = mockk() + val mockTask = mockk>() val mockDocumentSnapshot = mockk() { every { exists() } returns true @@ -306,25 +304,22 @@ public class ProfileConnectionTest { every { getBoolean("isPublic") } returns false every { getString("profilePictureUri") } returns "" } + val mockDocumentReference = mockk() + every { mockDocumentReference.get() } returns mockTask // Define behavior for the addOnSuccessListener method - every { mockTask.addOnSuccessListener(any>()) } answers + every { mockTask.addOnSuccessListener(any>()) } answers { - val listener = arg>(0) + val listener = arg>(0) // Define the behavior of the mock DocumentSnapshot here - listener.onSuccess(mockQuerySnapshot) + listener.onSuccess(mockDocumentSnapshot) mockTask } every { mockTask.addOnFailureListener(any()) } answers { mockTask } every { firebaseFirestore.collection(profileConnection.collectionName) } returns collectionReference - every { collectionReference.whereEqualTo("spotifyUid", "testUid") } returns query - every { query.get() } returns mockTask - - every { mockQuerySnapshot.isEmpty } returns false - every { mockQuerySnapshot.size() } returns 1 - every { mockQuerySnapshot.documents } returns listOf(mockDocumentSnapshot) + every { collectionReference.document("testUid") } returns mockDocumentReference val callback = mockk<(Boolean, Profile?) -> Unit>(relaxed = true) @@ -333,9 +328,17 @@ public class ProfileConnectionTest { verify { callback.invoke(true, any()) } // same but document size is 0 - every { mockQuerySnapshot.isEmpty } returns true - every { mockQuerySnapshot.size() } returns 0 - every { mockQuerySnapshot.documents } returns listOf() + every { mockTask.addOnSuccessListener(any>()) } answers + { + mockTask + } + every { mockTask.addOnFailureListener(any()) } answers + { + val listener = arg(0) + // Define the behavior of the mock DocumentSnapshot here + listener.onFailure(Exception("test")) + mockTask + } profileConnection.isUidExisting("testUid", callback) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/EditProfileTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/EditProfileTest.kt index e62ded25b..fe8c336a8 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/EditProfileTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/EditProfileTest.kt @@ -3,6 +3,7 @@ package ch.epfl.cs311.wanderwave.ui import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.lifecycle.viewModelScope import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController import ch.epfl.cs311.wanderwave.model.data.Profile import ch.epfl.cs311.wanderwave.model.remote.ProfileConnection import ch.epfl.cs311.wanderwave.model.spotify.SpotifyController @@ -52,10 +53,12 @@ class EditProfileTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withCompos @RelaxedMockK private lateinit var spotifyController: SpotifyController + @RelaxedMockK private lateinit var authenticationController: AuthenticationController + @Before fun setup() { mockDependencies() - viewModel = ProfileViewModel(profileRepository, spotifyController) + viewModel = ProfileViewModel(profileRepository, spotifyController, authenticationController) composeTestRule.setContent { EditProfileScreen(mockNavigationActions, viewModel) } } diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt index f6fc47db5..b161b3e0e 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt @@ -3,6 +3,7 @@ package ch.epfl.cs311.wanderwave.ui import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.lifecycle.viewModelScope import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController import ch.epfl.cs311.wanderwave.model.data.Profile import ch.epfl.cs311.wanderwave.model.remote.ProfileConnection import ch.epfl.cs311.wanderwave.model.spotify.SpotifyController @@ -51,10 +52,12 @@ class ProfileTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSup @RelaxedMockK private lateinit var spotifyController: SpotifyController + @RelaxedMockK private lateinit var authenticationController: AuthenticationController + @Before fun setup() { mockDependencies() - viewModel = ProfileViewModel(profileRepository, spotifyController) + viewModel = ProfileViewModel(profileRepository, spotifyController, authenticationController) composeTestRule.setContent { ProfileScreen(mockNavigationActions, viewModel) } } diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt index 7d9ca1fde..7c5b68e55 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt @@ -1,6 +1,7 @@ package ch.epfl.cs311.wanderwave.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController import ch.epfl.cs311.wanderwave.model.data.ListType import ch.epfl.cs311.wanderwave.model.data.Track import ch.epfl.cs311.wanderwave.model.remote.ProfileConnection @@ -43,10 +44,12 @@ class ProfileViewModelTest { @RelaxedMockK private lateinit var spotifyController: SpotifyController + @RelaxedMockK private lateinit var authenticationController: AuthenticationController + @Before fun setup() { Dispatchers.setMain(testDispatcher) - viewModel = ProfileViewModel(profileRepository, spotifyController) + viewModel = ProfileViewModel(profileRepository, spotifyController, authenticationController) } @After From a3e0e3d2486e262a3269a9f161cd8d8f81d7eb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Tue, 14 May 2024 00:54:00 +0200 Subject: [PATCH 3/8] Fix profile test --- .../ch/epfl/cs311/wanderwave/ui/ProfileTest.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt index b161b3e0e..002484292 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt @@ -85,6 +85,22 @@ class ProfileTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSup coEvery { profileRepository.deleteItem(any()) } just Runs coEvery { profileRepository.deleteItem(any()) } just Runs + coEvery { profileRepository.isUidExisting(any(), any()) } answers { + val callback = arg<(Boolean, Profile?) -> Unit>(1) + callback(true, Profile( + "My FirstName", + "My LastName", + "My Description", + 0, + true, + null, + "spotifyUid", + "firebaseUid", + listOf(), + listOf(), + )) + } + // Mocking SpotifyController coEvery { spotifyController.getChildren(any()) } returns flowOf(ListItem("", "", null, "", "", false, false)) From a192be7406a6985f0da50a1284058ac9b5365595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Tue, 14 May 2024 00:57:21 +0200 Subject: [PATCH 4/8] Formatting --- .../epfl/cs311/wanderwave/ui/ProfileTest.kt | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt index 002484292..9c4a00ea7 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt @@ -85,21 +85,24 @@ class ProfileTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSup coEvery { profileRepository.deleteItem(any()) } just Runs coEvery { profileRepository.deleteItem(any()) } just Runs - coEvery { profileRepository.isUidExisting(any(), any()) } answers { - val callback = arg<(Boolean, Profile?) -> Unit>(1) - callback(true, Profile( - "My FirstName", - "My LastName", - "My Description", - 0, - true, - null, - "spotifyUid", - "firebaseUid", - listOf(), - listOf(), - )) - } + coEvery { profileRepository.isUidExisting(any(), any()) } answers + { + val callback = arg<(Boolean, Profile?) -> Unit>(1) + callback( + true, + Profile( + "My FirstName", + "My LastName", + "My Description", + 0, + true, + null, + "spotifyUid", + "firebaseUid", + listOf(), + listOf(), + )) + } // Mocking SpotifyController coEvery { spotifyController.getChildren(any()) } returns From ed338e9792544c90fb4c2981fa88378ffcc5cb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Tue, 14 May 2024 01:31:21 +0200 Subject: [PATCH 5/8] Increase coverage --- .../viewmodel/ProfileViewModelTest.kt | 17 +++++++++++++++++ .../ui/screens/ProfileViewOnlyScreen.kt | 2 +- .../wanderwave/viewmodel/ProfileViewModel.kt | 4 ++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt index 7c5b68e55..5a9d4ad39 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt @@ -3,6 +3,7 @@ package ch.epfl.cs311.wanderwave.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController import ch.epfl.cs311.wanderwave.model.data.ListType +import ch.epfl.cs311.wanderwave.model.data.Profile import ch.epfl.cs311.wanderwave.model.data.Track import ch.epfl.cs311.wanderwave.model.remote.ProfileConnection import ch.epfl.cs311.wanderwave.model.spotify.SpotifyController @@ -11,6 +12,7 @@ import io.mockk.clearAllMocks import io.mockk.every import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit4.MockKRule +import io.mockk.verify import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -119,4 +121,19 @@ class ProfileViewModelTest { assertEquals(expectedListItem, result?.get(0)) assertEquals(expectedListItem, result2?.get(0)) } + + @Test + fun testCreateProfile() = runBlockingTest { + every { profileRepository.isUidExisting("firebaseUid", any()) } answers + { + val callback = arg<(Boolean, Profile?) -> Unit>(1) + callback(false, null) + } + + viewModel.getProfileByID("firebaseUid", true) + + verify { profileRepository.addItemWithId(any()) } + + viewModel.getProfileByID("firebaseUid", false) + } } diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileViewOnlyScreen.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileViewOnlyScreen.kt index 2ffb29aba..8e4568c1e 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileViewOnlyScreen.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/ui/screens/ProfileViewOnlyScreen.kt @@ -57,7 +57,7 @@ fun ProfileViewOnlyScreen( LaunchedEffect(profileId) { if (profileId != null) { - viewModel.getProfileByID(profileId) + viewModel.getProfileByID(profileId, false) } else { withContext(Dispatchers.Main) { navigationActions.navigateTo(Route.MAIN) diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt index 51cb8b845..e9ab45e6e 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModel.kt @@ -101,7 +101,7 @@ constructor( _isInPublicMode.value = !_isInPublicMode.value } - suspend fun getProfileByID(id: String, create: Boolean = false) { + suspend fun getProfileByID(id: String, create: Boolean) { repository.isUidExisting(id) { exists, fetchedProfile -> if (exists) { _profile.value = fetchedProfile!! @@ -117,7 +117,7 @@ constructor( } } - suspend fun getProfileOfCurrentUser(create: Boolean = false) { + suspend fun getProfileOfCurrentUser(create: Boolean) { val currentUserId = authenticationController.getUserData()!!.id getProfileByID(currentUserId, create) } From 8147bf0ac1039ac995e810011af4c881ce354962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Sun, 19 May 2024 15:02:08 +0200 Subject: [PATCH 6/8] Fix tests --- .../wanderwave/viewmodel/ProfileViewModelTest.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt index 575451ee0..b3389073f 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/ProfileViewModelTest.kt @@ -202,7 +202,7 @@ class ProfileViewModelTest { every { profileRepository.getItem(testId) } returns testFlow // Act - viewModel.getProfileByID(testId) + viewModel.getProfileByID(testId, false) // Assert assertEquals(testProfile, viewModel.profile.value) @@ -213,7 +213,7 @@ class ProfileViewModelTest { val testFlowError = flowOf(Result.failure(Exception("Test Exception"))) every { profileRepository.getItem(testId) } returns testFlowError - viewModel.getProfileByID(testId) + viewModel.getProfileByID(testId, false) assertEquals( ProfileViewModel.UIState(profile = null, isLoading = false, error = "Test Exception"), viewModel.uiState.value) @@ -221,11 +221,8 @@ class ProfileViewModelTest { @Test fun testCreateProfile() = runBlockingTest { - every { profileRepository.isUidExisting("firebaseUid", any()) } answers - { - val callback = arg<(Boolean, Profile?) -> Unit>(1) - callback(false, null) - } + every { profileRepository.getItem(any()) } returns + flowOf(Result.failure(Exception("Document does not exist"))) viewModel.getProfileByID("firebaseUid", true) From deeb322a127a809272995f835ed185a99e23f667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Sun, 19 May 2024 15:05:11 +0200 Subject: [PATCH 7/8] Remove now unnecessary isUidExisting --- .../wanderwave/model/ProfileConnectionTest.kt | 66 ------------------- .../epfl/cs311/wanderwave/ui/ProfileTest.kt | 19 ------ .../model/remote/ProfileConnection.kt | 13 ---- .../model/repository/ProfileRepository.kt | 5 +- 4 files changed, 1 insertion(+), 102 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt index 4b65ed2d4..7d89f4811 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/ProfileConnectionTest.kt @@ -5,8 +5,6 @@ import ch.epfl.cs311.wanderwave.model.data.Profile import ch.epfl.cs311.wanderwave.model.data.Track import ch.epfl.cs311.wanderwave.model.remote.ProfileConnection import ch.epfl.cs311.wanderwave.model.remote.TrackConnection -import com.google.android.gms.tasks.OnFailureListener -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 @@ -297,68 +295,4 @@ public class ProfileConnectionTest { assert(result.isFailure) } } - - @Test - fun testIsUidExisting() { - runBlocking { - withTimeout(3000) { - // Pass the mock Firestore instance to your BeaconConnection - - val collectionReference = mockk() - - val mockTask = mockk>() - val mockDocumentSnapshot = - mockk() { - every { exists() } returns true - every { getString("spotifyUid") } returns "testUid" - every { getString("firebaseUid") } returns "testFirebaseUid" - every { getString("firstName") } returns "testFirstName" - every { getString("lastName") } returns "testLastName" - every { getString("description") } returns "testDescription" - every { getLong("numberOfLikes") } returns 0 - every { getBoolean("isPublic") } returns false - every { getString("profilePictureUri") } returns "" - } - val mockDocumentReference = mockk() - every { mockDocumentReference.get() } returns mockTask - - // Define behavior for the addOnSuccessListener method - every { mockTask.addOnSuccessListener(any>()) } answers - { - val listener = arg>(0) - // Define the behavior of the mock DocumentSnapshot here - listener.onSuccess(mockDocumentSnapshot) - mockTask - } - every { mockTask.addOnFailureListener(any()) } answers { mockTask } - - every { firebaseFirestore.collection(profileConnection.collectionName) } returns - collectionReference - every { collectionReference.document("testUid") } returns mockDocumentReference - - val callback = mockk<(Boolean, Profile?) -> Unit>(relaxed = true) - - profileConnection.isUidExisting("testUid", callback) - - verify { callback.invoke(true, any()) } - - // same but document size is 0 - every { mockTask.addOnSuccessListener(any>()) } answers - { - mockTask - } - every { mockTask.addOnFailureListener(any()) } answers - { - val listener = arg(0) - // Define the behavior of the mock DocumentSnapshot here - listener.onFailure(Exception("test")) - mockTask - } - - profileConnection.isUidExisting("testUid", callback) - - verify { callback.invoke(false, null) } - } - } - } } diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt index 9c4a00ea7..b161b3e0e 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/ProfileTest.kt @@ -85,25 +85,6 @@ class ProfileTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComposeSup coEvery { profileRepository.deleteItem(any()) } just Runs coEvery { profileRepository.deleteItem(any()) } just Runs - coEvery { profileRepository.isUidExisting(any(), any()) } answers - { - val callback = arg<(Boolean, Profile?) -> Unit>(1) - callback( - true, - Profile( - "My FirstName", - "My LastName", - "My Description", - 0, - true, - null, - "spotifyUid", - "firebaseUid", - listOf(), - listOf(), - )) - } - // Mocking SpotifyController coEvery { spotifyController.getChildren(any()) } returns flowOf(ListItem("", "", null, "", "", false, false)) diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt index 9e681025b..b8ec55874 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/ProfileConnection.kt @@ -1,6 +1,5 @@ package ch.epfl.cs311.wanderwave.model.remote -import android.util.Log import ch.epfl.cs311.wanderwave.model.data.Profile import ch.epfl.cs311.wanderwave.model.data.Track import ch.epfl.cs311.wanderwave.model.repository.ProfileRepository @@ -31,18 +30,6 @@ class ProfileConnection( override val db = database private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - override fun isUidExisting(firebaseUid: String, callback: (Boolean, Profile?) -> Unit) { - db.collection("users") - .document(firebaseUid) - .get() - .addOnSuccessListener { document -> callback(true, documentToItem(document)) } - .addOnFailureListener { exception -> - Log.w("Firestore", "Error getting documents: ", exception) - callback(false, null) // Assuming failure means document doesn't exist - } - } - - // Document to Profile override fun documentToItem(document: DocumentSnapshot): Profile? { return Profile.from(document) } diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/ProfileRepository.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/ProfileRepository.kt index e37c13c47..51991473a 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/ProfileRepository.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/ProfileRepository.kt @@ -3,7 +3,4 @@ package ch.epfl.cs311.wanderwave.model.repository import ch.epfl.cs311.wanderwave.model.data.Profile // ProfileRepository.kt -interface ProfileRepository : FirebaseRepository { - - fun isUidExisting(spotifyUid: String, callback: (Boolean, Profile?) -> Unit) -} +interface ProfileRepository : FirebaseRepository {} From 2af5f161ffa614b833d92ae532e2bc172cf4a69b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoric=20Z=C3=BCger?= <79338025+yzueger@users.noreply.github.com> Date: Sun, 19 May 2024 18:16:03 +0200 Subject: [PATCH 8/8] Set the users profile correctly when adding a track to a beacon --- .../wanderwave/model/BeaconConnectionTest.kt | 48 +++++++++++++++++-- .../cs311/wanderwave/ui/BeaconScreenTest.kt | 10 +++- .../viewmodel/BeaconScreenViewModelTest.kt | 25 ++++++++-- .../model/remote/BeaconConnection.kt | 43 ++++++++++++----- .../model/repository/BeaconRepository.kt | 7 ++- .../wanderwave/viewmodel/BeaconViewModel.kt | 7 ++- 6 files changed, 114 insertions(+), 26 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/BeaconConnectionTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/BeaconConnectionTest.kt index 2d50902bb..3e588ca1e 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/BeaconConnectionTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/model/BeaconConnectionTest.kt @@ -469,9 +469,28 @@ public class BeaconConnectionTest { hashMapOf( "id" to getTestBeacon.id, "location" to getTestBeacon.location.toMap(), - "tracks" to getTestBeacon.profileAndTrack.map { it.toMap() }) + "tracks" to + getTestBeacon.profileAndTrack.map { + hashMapOf( + "creator" to firestore.collection("users").document(profile.firebaseUid), + "track" to firestore.collection("tracks").document(track.id)) + }) + + every { mockTransaction.get(any()) } answers + { + val reference = arg(0) + + when { + reference.path.contains("beacons") -> mockDocumentSnapshot + reference.path.contains("users") -> + mockk() { every { getData() } returns profile.toMap() } + reference.path.contains("tracks") -> + mockk() { every { getData() } returns track.toMap() } + reference.path.equals("") -> mockDocumentSnapshot + else -> throw IllegalStateException("Invalid reference path: ${reference.path}") + } + } - every { mockTransaction.get(any()) } returns mockDocumentSnapshot every { mockTransaction.update(any(), any(), any()) } answers { mockTransaction @@ -481,7 +500,26 @@ public class BeaconConnectionTest { every { mockDocumentSnapshot.id } returns getTestBeacon.id every { mockDocumentSnapshot.get("location") } returns getTestBeacon.location.toMap() every { mockDocumentSnapshot.get("tracks") } returns - getTestBeacon.profileAndTrack.map { it.toMap() } + getTestBeacon.profileAndTrack.map { + hashMapOf( + "creator" to firestore.collection("users").document(profile.firebaseUid), + "track" to firestore.collection("tracks").document(track.id)) + } + + profile.toMap().forEach { (key, value) -> + every { mockDocumentSnapshot.get(key) } returns value + (value as? String)?.let { every { mockDocumentSnapshot.getString(key) } returns it } + (value as? Int)?.let { every { mockDocumentSnapshot.getLong(key) } returns it.toLong() } + (value as? Boolean)?.let { every { mockDocumentSnapshot.getBoolean(key) } returns it } + } + track.toMap().forEach { (key, value) -> + every { mockDocumentSnapshot.get(key) } returns value + (value as? String)?.let { every { mockDocumentSnapshot.getString(key) } returns it } + (value as? Int)?.let { every { mockDocumentSnapshot.getLong(key) } returns it.toLong() } + (value as? Boolean)?.let { every { mockDocumentSnapshot.getBoolean(key) } returns it } + } + + every { mockDocumentSnapshot.getLong(any()) } returns 0 // Define behavior for the addOnSuccessListener method every { mockTask.addOnSuccessListener(any>()) } answers @@ -503,7 +541,7 @@ public class BeaconConnectionTest { } // Call the function under test - beaconConnection.addTrackToBeacon(beacon.id, track, {}) + beaconConnection.addTrackToBeacon(beacon.id, track, "testing-uid", {}) verify { firestore.runTransaction(any()) } verify { mockTransaction.get(any()) } @@ -573,7 +611,7 @@ public class BeaconConnectionTest { // Call the function under test try { - beaconConnection.addTrackToBeacon(beacon.id, track, {}) + beaconConnection.addTrackToBeacon(beacon.id, track, "testing-uid", {}) fail("Should have thrown an exception") } catch (e: Exception) { // Verify that the exception is thrown diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/BeaconScreenTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/BeaconScreenTest.kt index b443907f3..084f5b6ec 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/BeaconScreenTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/ui/BeaconScreenTest.kt @@ -3,6 +3,8 @@ package ch.epfl.cs311.wanderwave.ui import androidx.compose.ui.test.junit4.createComposeRule import androidx.navigation.NavHostController import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationUserData import ch.epfl.cs311.wanderwave.model.data.Beacon import ch.epfl.cs311.wanderwave.model.data.Location import ch.epfl.cs311.wanderwave.model.data.Profile @@ -41,6 +43,7 @@ class BeaconScreenTest { @RelaxedMockK private lateinit var trackRepository: TrackRepository @RelaxedMockK private lateinit var mockNavController: NavHostController @RelaxedMockK private lateinit var mockSpotifyController: SpotifyController + @RelaxedMockK private lateinit var mockAuthenticationController: AuthenticationController @Before fun setup() { @@ -74,7 +77,12 @@ class BeaconScreenTest { val connectResult = SpotifyController.ConnectResult.SUCCESS every { mockSpotifyController.connectRemote() } returns flowOf(connectResult) - val viewModel = BeaconViewModel(trackRepository, beaconConnection, mockSpotifyController) + every { mockAuthenticationController.getUserData() } returns + AuthenticationUserData("test-uid", "test-email", "test-name", "test-photo-url") + + val viewModel = + BeaconViewModel( + trackRepository, beaconConnection, mockSpotifyController, mockAuthenticationController) composeTestRule.setContent { BeaconScreen(beaconId, mockNavigationActions, viewModel) } diff --git a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconScreenViewModelTest.kt b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconScreenViewModelTest.kt index 148b5d01c..000d248e4 100644 --- a/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconScreenViewModelTest.kt +++ b/app/src/androidTest/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconScreenViewModelTest.kt @@ -1,5 +1,7 @@ package ch.epfl.cs311.wanderwave.viewmodel +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationUserData import ch.epfl.cs311.wanderwave.model.data.Beacon import ch.epfl.cs311.wanderwave.model.data.ListType import ch.epfl.cs311.wanderwave.model.data.Location @@ -47,6 +49,7 @@ class BeaconScreenViewModelTest { @get:Rule val mockkRule = MockKRule(this) @RelaxedMockK private lateinit var beaconConnection: BeaconConnection @RelaxedMockK private lateinit var mockSpotifyController: SpotifyController + @RelaxedMockK private lateinit var mockAuthenticationController: AuthenticationController lateinit var viewModel: BeaconViewModel val testDispatcher = TestCoroutineDispatcher() @@ -57,7 +60,14 @@ class BeaconScreenViewModelTest { @Before fun setup() { Dispatchers.setMain(testDispatcher) - viewModel = BeaconViewModel(trackRepository, beaconRepository, mockSpotifyController) + + every { mockAuthenticationController.isSignedIn() } returns true + every { mockAuthenticationController.getUserData() } returns + AuthenticationUserData("uid", "email", "name", "http://photoUrl/img.jpg") + + viewModel = + BeaconViewModel( + trackRepository, beaconRepository, mockSpotifyController, mockAuthenticationController) } @OptIn(ExperimentalCoroutinesApi::class) @@ -79,16 +89,19 @@ class BeaconScreenViewModelTest { fun canConstructWithNoErrors() { val connectResult = SpotifyController.ConnectResult.SUCCESS every { mockSpotifyController.connectRemote() } returns flowOf(connectResult) - BeaconViewModel(trackRepository, beaconConnection, mockSpotifyController) + BeaconViewModel( + trackRepository, beaconConnection, mockSpotifyController, mockAuthenticationController) } @Test fun addTrackToBeaconTest() { - val viewModel = BeaconViewModel(trackRepository, beaconConnection, mockSpotifyController) + val viewModel = + BeaconViewModel( + trackRepository, beaconConnection, mockSpotifyController, mockAuthenticationController) val track = Track("trackId", "trackName", "trackArtist") viewModel.addTrackToBeacon("beaconId", track, {}) - verify { beaconConnection.addTrackToBeacon(any(), any(), any()) } + verify { beaconConnection.addTrackToBeacon(any(), any(), any(), any()) } } @OptIn(ExperimentalCoroutinesApi::class) @@ -137,7 +150,9 @@ class BeaconScreenViewModelTest { @Test fun canSelectTracks() { - val viewModel = BeaconViewModel(trackRepository, beaconConnection, mockSpotifyController) + val viewModel = + BeaconViewModel( + trackRepository, beaconConnection, mockSpotifyController, mockAuthenticationController) val track = Track("trackId", "trackName", "trackArtist") viewModel.selectTrack(track) diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/BeaconConnection.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/BeaconConnection.kt index 45c136e8b..0b6d0fd1e 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/BeaconConnection.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/model/remote/BeaconConnection.kt @@ -160,15 +160,31 @@ class BeaconConnection( return beaconMap } - override fun addTrackToBeacon(beaconId: String, track: Track, onComplete: (Boolean) -> Unit) { + override fun addTrackToBeacon( + beaconId: String, + track: Track, + profileUid: String, + onComplete: (Boolean) -> Unit + ) { val beaconRef = db.collection("beacons").document(beaconId) - val profileRef = db.collection("users").document("My Firebase UID") db.runTransaction { transaction -> - val snapshot = transaction[beaconRef] + val snapshot: DocumentSnapshot = transaction[beaconRef] val beacon = Beacon.from(snapshot) + val tracks = snapshot.get("tracks") as? List> ?: listOf() + val associations = + tracks.mapNotNull { + val creatorRef = it["creator"] + val trackRef = it["track"] + + val creator = creatorRef?.let { Profile.from(transaction[it]) } + val track = trackRef?.let { Track.from(transaction[it]) } + + track?.let { ProfileTrackAssociation(creator, it) } + } + beacon?.let { val newTracks = - ArrayList(it.profileAndTrack).apply { + ArrayList(associations).apply { add( ProfileTrackAssociation( Profile( @@ -178,19 +194,22 @@ class BeaconConnection( 0, false, null, - "My Firebase UID", - "My Firebase UID"), + "sample spotify uid", + profileUid), track)) } transaction.update( beaconRef, "tracks", newTracks.map { profileAndTrack -> - hashMapOf( - "creator" to - db.collection("users") - .document(profileAndTrack.profile?.firebaseUid ?: profileRef.id), - "track" to db.collection("tracks").document(profileAndTrack.track.id)) + if (profileAndTrack.profile == null) { + hashMapOf("track" to db.collection("tracks").document(profileAndTrack.track.id)) + } else { + hashMapOf( + "creator" to + db.collection("users").document(profileAndTrack.profile.firebaseUid), + "track" to db.collection("tracks").document(profileAndTrack.track.id)) + } }) // After updating Firestore, save the track addition locally coroutineScope.launch { @@ -202,7 +221,7 @@ class BeaconConnection( trackId = track.id, timestamp = System.currentTimeMillis())) } - } ?: throw Exception("Beacon not found") + } } .addOnSuccessListener { onComplete(true) } .addOnFailureListener { onComplete(false) } diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/BeaconRepository.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/BeaconRepository.kt index a83c39674..b88774054 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/BeaconRepository.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/model/repository/BeaconRepository.kt @@ -9,7 +9,12 @@ interface BeaconRepository : FirebaseRepository { fun getAll(): Flow> - fun addTrackToBeacon(beaconId: String, track: Track, onComplete: (Boolean) -> Unit) + fun addTrackToBeacon( + beaconId: String, + track: Track, + profileUid: String, + onComplete: (Boolean) -> Unit + ) suspend fun addItemAndGetId(item: Beacon): String? } diff --git a/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconViewModel.kt b/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconViewModel.kt index 54b94ab4e..fae319bb1 100644 --- a/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconViewModel.kt +++ b/app/src/main/java/ch/epfl/cs311/wanderwave/viewmodel/BeaconViewModel.kt @@ -3,6 +3,7 @@ package ch.epfl.cs311.wanderwave.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import ch.epfl.cs311.wanderwave.model.auth.AuthenticationController import ch.epfl.cs311.wanderwave.model.data.Beacon import ch.epfl.cs311.wanderwave.model.data.ListType import ch.epfl.cs311.wanderwave.model.data.Location @@ -30,7 +31,8 @@ class BeaconViewModel constructor( private val trackRepository: TrackRepository, private val beaconRepository: BeaconRepository, - private val spotifyController: SpotifyController + private val spotifyController: SpotifyController, + private val authenticationController: AuthenticationController ) : ViewModel(), SpotifySongsActions { private var _uiState = MutableStateFlow(UIState()) @@ -94,7 +96,8 @@ constructor( // Call the BeaconConnection's addTrackToBeacon with the provided beaconId and track val correctTrack = track.copy(id = "spotify:track:" + track.id) trackRepository.addItemsIfNotExist(listOf(correctTrack)) - beaconRepository.addTrackToBeacon(beaconId, correctTrack, onComplete) + beaconRepository.addTrackToBeacon( + beaconId, correctTrack, authenticationController.getUserData()!!.id, onComplete) } // Function to add a track to a song list