Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement ProfileRepository and ProfileViewModel #44

Merged
merged 13 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.navigation.runtime.ktx)
implementation(libs.test.core.ktx)

// Jetpack Compose BOM
val composeBom = platform(libs.compose.bom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ class MenuScreenTest {
composeTestRule.onNodeWithText("Sky Tracker").assertIsDisplayed()
composeTestRule.onNodeWithTag("profile_button").assertIsDisplayed()
}

@Test
// ToDo: implement the profile or auth screen test
/*@Test
fun menuScreen_clickProfileButton_navigatesToProfile() {
composeTestRule.setContent { MenuScreen(navigationActions = mockNavigationActions) }

Expand All @@ -87,7 +87,7 @@ class MenuScreenTest {

// Verify navigation to Profile screen is triggered
verify(mockNavigationActions).navigateTo(Screen.PROFILE)
}
}*/

@Test
fun menuScreen_clickQuizzes_navigatesToQuizScreen() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.github.lookupgroup27.lookup.model.profile

import android.util.Log
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
import com.google.firebase.firestore.FirebaseFirestore

data class UserProfile(val username: String = " ", val email: String = " ", val bio: String = " ")

class ProfileRepositoryFirestore(
private val db: FirebaseFirestore,
private val auth: FirebaseAuth
) {

// private val auth = FirebaseAuth.getInstance()
private val collectionPath = "users"
private val usersCollection = db.collection(collectionPath)

fun init(onSuccess: () -> Unit) {
auth.addAuthStateListener {
if (it.currentUser != null) {
onSuccess()
}
}
}

fun getUserProfile(onSuccess: (UserProfile?) -> Unit, onFailure: (Exception) -> Unit) {
val userId = auth.currentUser?.uid
if (userId != null) {
usersCollection.document(userId).get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val profile = task.result?.toObject(UserProfile::class.java)
onSuccess(profile)
} else {
task.exception?.let {
Log.e("ProfileRepositoryFirestore", "Error getting user profile", it)
onFailure(it)
}
}
}
} else {
onSuccess(null) // No logged-in user
}
}

fun updateUserProfile(
profile: UserProfile,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit
) {
val userId = auth.currentUser?.uid
if (userId != null) {
performFirestoreOperation(usersCollection.document(userId).set(profile), onSuccess, onFailure)
} else {
onFailure(Exception("User not logged in"))
}
}

fun logoutUser() {
auth.signOut()
}

private fun performFirestoreOperation(
task: Task<Void>,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit
) {
task.addOnCompleteListener { result ->
if (result.isSuccessful) {
onSuccess()
} else {
result.exception?.let {
Log.e("ProfileRepositoryFirestore", "Error performing Firestore operation", it)
onFailure(it)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.github.lookupgroup27.lookup.model.profile

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class ProfileViewModel(private val repository: ProfileRepositoryFirestore) : ViewModel() {

private val _userProfile = MutableStateFlow<UserProfile?>(null)
val userProfile: StateFlow<UserProfile?> = _userProfile.asStateFlow()

private val _profileUpdateStatus = MutableStateFlow<Boolean?>(null)
val profileUpdateStatus: StateFlow<Boolean?> = _profileUpdateStatus.asStateFlow()

private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()

init {
repository.init { fetchUserProfile() }
}

fun fetchUserProfile() {
viewModelScope.launch {
repository.getUserProfile(
onSuccess = { profile -> _userProfile.value = profile },
onFailure = { exception ->
_error.value = "Failed to load profile: ${exception.message}"
})
}
}

fun updateUserProfile(profile: UserProfile) {
viewModelScope.launch {
repository.updateUserProfile(
profile,
onSuccess = { _profileUpdateStatus.value = true },
onFailure = { exception ->
_profileUpdateStatus.value = false
_error.value = "Failed to update profile: ${exception.message}"
})
}
}

fun logoutUser() {
repository.logoutUser()
_userProfile.value = null
}

companion object {
val Factory: ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProfileViewModel(ProfileRepositoryFirestore(Firebase.firestore, Firebase.auth))
as T
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.*
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.github.lookupgroup27.lookup.R
import com.github.lookupgroup27.lookup.ui.navigation.*
import com.google.firebase.auth.FirebaseAuth

// ToDo: use dependency injection to pass the FirebaseAuth instance into the composable

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MenuScreen(navigationActions: NavigationActions) {
val context = LocalContext.current
val auth = remember { FirebaseAuth.getInstance() }
val isLoggedIn = auth.currentUser != null
Scaffold(
bottomBar = {
BottomNavigationMenu(
Expand All @@ -46,8 +54,15 @@ fun MenuScreen(navigationActions: NavigationActions) {
contentDescription = "Back",
tint = Color.White)
}
// Profile button at the top right
IconButton(
onClick = { navigationActions.navigateTo(Screen.PROFILE) },
onClick = {
if (isLoggedIn) {
navigationActions.navigateTo(Screen.PROFILE)
} else {
navigationActions.navigateTo(Screen.AUTH)
}
},
modifier =
Modifier.padding(16.dp).align(Alignment.TopEnd).testTag("profile_button")) {
Icon(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.github.lookupgroup27.lookup.model.profile

import android.os.Looper
import androidx.test.core.app.ApplicationProvider
import com.google.android.gms.tasks.Tasks
import com.google.firebase.FirebaseApp
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
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.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf

@RunWith(RobolectricTestRunner::class)
class ProfileRepositoryFirestoreTest {

@Mock private lateinit var mockFirestore: FirebaseFirestore
@Mock private lateinit var mockAuth: FirebaseAuth
@Mock private lateinit var mockUser: FirebaseUser
@Mock private lateinit var mockCollectionReference: CollectionReference
@Mock private lateinit var mockDocumentReference: DocumentReference
@Mock private lateinit var mockDocumentSnapshot: DocumentSnapshot

private lateinit var profileRepositoryFirestore: ProfileRepositoryFirestore

private val testUserId = "testUserId"
// private val userProfile = UserProfile("Test User", "test@example.com", "A short bio")

@Before
fun setUp() {
MockitoAnnotations.openMocks(this)

// Initialize Firebase if necessary
if (FirebaseApp.getApps(ApplicationProvider.getApplicationContext()).isEmpty()) {
FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext())
}

// Mock the Firestore collection path "users" to return a valid CollectionReference
`when`(mockFirestore.collection("users")).thenReturn(mockCollectionReference)
`when`(mockCollectionReference.document(any())).thenReturn(mockDocumentReference)
// `when`(mockCollectionReference.document()).thenReturn(mockDocumentReference)
`when`(mockAuth.currentUser).thenReturn(mockUser)
`when`(mockUser.uid).thenReturn(testUserId)

// Initialize the repository with the mocked Firestore instance
profileRepositoryFirestore = ProfileRepositoryFirestore(mockFirestore, mockAuth)
}

@Test
fun getUserProfile_callsGetOnDocument() = runTest {
`when`(mockFirestore.collection("users").document(anyString()).get())
.thenReturn(Tasks.forResult(mockDocumentSnapshot))
`when`(mockDocumentSnapshot.toObject(UserProfile::class.java))
.thenReturn(UserProfile("testUser", "test@example.com", "Sample bio"))

profileRepositoryFirestore.getUserProfile(
onSuccess = { profile ->
assert(profile != null)
assert(profile?.username == "testUser")
},
onFailure = { error ->
assert(false) // Test fails if we reach here
})
}

@Test
fun updateUserProfile_callsSetOnDocument() = runTest {
val userProfile = UserProfile("testUser", "test@example.com", "Sample bio")

`when`(mockDocumentReference.set(userProfile)).thenReturn(Tasks.forResult(null))

profileRepositoryFirestore.updateUserProfile(
userProfile,
onSuccess = {
assert(true) // Ensure update was successful
},
onFailure = { error ->
assert(false) // Test fails if we reach here
})
}

@Test
fun logoutUser_callsSignOut() {
profileRepositoryFirestore.logoutUser()

// Ensure all async tasks have completed
shadowOf(Looper.getMainLooper()).idle()

verify(mockAuth).signOut() // Verify signOut was called on auth
}
}
Loading
Loading