diff --git a/app/src/main/java/com/android/periodpals/model/alert/Alert.kt b/app/src/main/java/com/android/periodpals/model/alert/Alert.kt index fb05a9ca4..013468ad6 100644 --- a/app/src/main/java/com/android/periodpals/model/alert/Alert.kt +++ b/app/src/main/java/com/android/periodpals/model/alert/Alert.kt @@ -16,7 +16,7 @@ package com.android.periodpals.model.alert */ data class Alert( val id: String?, // given when created in supabase - val uid: String?, + val uid: String, val name: String, val product: Product, val urgency: Urgency, @@ -29,7 +29,8 @@ data class Alert( /** Enum class representing the product requested with the alert. */ enum class Product { TAMPON, - PAD + PAD, + TAMPON_AND_PAD, } /** Enum class representing the urgency level of the alert. */ diff --git a/app/src/main/java/com/android/periodpals/model/alert/AlertDto.kt b/app/src/main/java/com/android/periodpals/model/alert/AlertDto.kt index 3a06e19e2..bf032f111 100644 --- a/app/src/main/java/com/android/periodpals/model/alert/AlertDto.kt +++ b/app/src/main/java/com/android/periodpals/model/alert/AlertDto.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.Serializable @Serializable data class AlertDto( @SerialName("id") val id: String?, - @SerialName("uid") val uid: String?, + @SerialName("uid") val uid: String, @SerialName("name") val name: String, @SerialName("product") val product: Product, @SerialName("urgency") val urgency: Urgency, diff --git a/app/src/main/java/com/android/periodpals/model/alert/AlertModel.kt b/app/src/main/java/com/android/periodpals/model/alert/AlertModel.kt index 660203dd7..3239e0fac 100644 --- a/app/src/main/java/com/android/periodpals/model/alert/AlertModel.kt +++ b/app/src/main/java/com/android/periodpals/model/alert/AlertModel.kt @@ -9,11 +9,10 @@ interface AlertModel { * Adds a new alert. * * @param alert The alert to be added. - * @param onSuccess Callback function to be called on successful addition, with the ID of the - * created alert as a parameter. + * @param onSuccess Callback function to be called on successful addition * @param onFailure Callback function to be called on failure, with the exception as a parameter. */ - suspend fun addAlert(alert: Alert, onSuccess: (String) -> Unit, onFailure: (Exception) -> Unit) + suspend fun addAlert(alert: Alert, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) /** * Retrieves an alert by its ID. diff --git a/app/src/main/java/com/android/periodpals/model/alert/AlertModelSupabase.kt b/app/src/main/java/com/android/periodpals/model/alert/AlertModelSupabase.kt index a9939f04e..66c08d8bf 100644 --- a/app/src/main/java/com/android/periodpals/model/alert/AlertModelSupabase.kt +++ b/app/src/main/java/com/android/periodpals/model/alert/AlertModelSupabase.kt @@ -28,7 +28,7 @@ class AlertModelSupabase( */ override suspend fun addAlert( alert: Alert, - onSuccess: (String) -> Unit, + onSuccess: () -> Unit, onFailure: (Exception) -> Unit ) { try { @@ -40,7 +40,7 @@ class AlertModelSupabase( val insertedAlert = insertedAlertDto.toAlert() if (insertedAlert.id != null) { Log.d(TAG, "addAlert: Success") - onSuccess(insertedAlert.id) + onSuccess() } else { Log.e(TAG, "addAlert: fail to create alert: ID is null") onFailure(Exception("ID is null")) diff --git a/app/src/main/java/com/android/periodpals/model/alert/AlertViewModel.kt b/app/src/main/java/com/android/periodpals/model/alert/AlertViewModel.kt new file mode 100644 index 000000000..6076a3b0f --- /dev/null +++ b/app/src/main/java/com/android/periodpals/model/alert/AlertViewModel.kt @@ -0,0 +1,142 @@ +package com.android.periodpals.model.alert + +import android.util.Log +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch + +private const val TAG = "AlertViewModel" + +/** + * ViewModel for managing alert data. + * + * @property alertModelSupabase The repository used for loading and saving alerts. + * @property _alerts Mutable state holding the list of alerts. + * @property alerts Public state exposing the list of alerts. + */ +class AlertViewModel(private val alertModelSupabase: AlertModelSupabase) : ViewModel() { + // remove this? + private var _alerts = mutableStateOf?>(listOf()) + val alerts: State?> = _alerts + + /** + * Creates a new alert. + * + * @param alert The alert to be created. + */ + fun createAlert(alert: Alert) { + viewModelScope.launch { + alertModelSupabase.addAlert( + alert = alert, + onSuccess = { + Log.d(TAG, "createAlert: Success") + getAllAlerts() // refresh the alerts list + }, + onFailure = { e -> Log.e(TAG, "createAlert: fail to create alert: ${e.message}") }) + } + } + + /** + * Retrieves an alert by its ID. + * + * @param idAlert The ID of the alert to be retrieved. + * @return The alert if found, null otherwise. + */ + fun getAlert(idAlert: String): Alert? { + var alert: Alert? = null + viewModelScope.launch { + alertModelSupabase.getAlert( + idAlert = idAlert, + onSuccess = { fetched -> + Log.d(TAG, "getAlert: Success") + alert = fetched + }, + onFailure = { e -> Log.e(TAG, "getAlert: fail to get alert: ${e.message}") }) + } + return alert + } + + /** + * Retrieves all alerts. + * + * @return The list of all alerts. + */ + fun getAllAlerts(): List? { + var alertsList: List? = null + viewModelScope.launch { + alertModelSupabase.getAllAlerts( + onSuccess = { alerts -> + Log.d(TAG, "getAllAlerts: Success") + _alerts.value = alerts + alertsList = alerts + }, + onFailure = { e -> Log.e(TAG, "getAllAlerts: fail to get alerts: ${e.message}") }) + } + return alertsList + } + + fun getPalAlerts(uid: String): List? { + val palAlertList: List? = getAllAlerts() + return palAlertList?.filter { it.uid != uid } + } + + /** + * Retrieves alerts for a specific user by their UID. + * + * @param uid The UID of the user. + * @return The list of alerts for the user. + */ + fun getAlertsByUser(uid: String): List? { + var alertsList: List? = null + viewModelScope.launch { + alertModelSupabase.getAlertsFilteredBy( + // ideally the uid would not be passed as argument and instead we could get uid by + // UserViewModel.currentUser?.uid + cond = { eq("uid", uid) }, + onSuccess = { alerts -> + Log.d(TAG, "getMyAlerts: Success") + alertsList = alerts + }, + onFailure = { e -> Log.e(TAG, "getMyAlerts: fail to get alerts: ${e.message}") }) + } + return alertsList + } + + /** + * Updates an existing alert. + * + * @param alert The alert with updated parameters. + */ + fun updateAlert(alert: Alert) { + viewModelScope.launch { + alertModelSupabase.updateAlert( + alert = alert, + onSuccess = { + Log.d(TAG, "updateAlert: Success") + getAllAlerts() + }, + onFailure = { e -> Log.e(TAG, "updateAlert: fail to update alert: ${e.message}") }) + } + } + + /** + * Deletes an alert. + * + * @param alert The alert to be deleted. + */ + fun deleteAlert(alert: Alert) { + viewModelScope.launch { + alert.id?.let { + alertModelSupabase.deleteAlertById( + idAlert = it, + onSuccess = { + Log.d(TAG, "deleteAlert: Success") + getAllAlerts() + }, + onFailure = { e -> Log.e(TAG, "deleteAlert: fail to delete alert: ${e.message}") }) + } ?: run { Log.e(TAG, "deleteAlert: fail to delete alert: id of the Alert is null") } + } + } +} diff --git a/app/src/test/java/com/android/periodpals/model/alert/AlertModelSupabaseTest.kt b/app/src/test/java/com/android/periodpals/model/alert/AlertModelSupabaseTest.kt index 6ba345eb5..6ceca95e8 100644 --- a/app/src/test/java/com/android/periodpals/model/alert/AlertModelSupabaseTest.kt +++ b/app/src/test/java/com/android/periodpals/model/alert/AlertModelSupabaseTest.kt @@ -82,14 +82,14 @@ class AlertModelSupabaseTest { @Test fun addAlertSuccess() = runBlocking { - var result = "" + var result = false alertModelSupabase.addAlert( alert = defaultAlert, - onSuccess = { result = it }, // Ensuring match with test expectation + onSuccess = { result = true }, // Ensuring match with test expectation onFailure = { fail("should not call onFailure") }) - assertEquals(defaultAlert.id, result) + assertEquals(true, result) } @Test diff --git a/app/src/test/java/com/android/periodpals/model/alert/AlertViewModelTest.kt b/app/src/test/java/com/android/periodpals/model/alert/AlertViewModelTest.kt new file mode 100644 index 000000000..d6571becc --- /dev/null +++ b/app/src/test/java/com/android/periodpals/model/alert/AlertViewModelTest.kt @@ -0,0 +1,372 @@ +package com.android.periodpals.model.alert + +import com.android.periodpals.MainCoroutineRule +import io.github.jan.supabase.postgrest.query.filter.PostgrestFilterBuilder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.LocalDateTime +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.doAnswer +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any + +@OptIn(ExperimentalCoroutinesApi::class) +class AlertViewModelTest { + @Mock private lateinit var alertModelSupabase: AlertModelSupabase + private lateinit var viewModel: AlertViewModel + + @ExperimentalCoroutinesApi @get:Rule var mainCoroutineRule = MainCoroutineRule() + + companion object { + const val ID = "idAlert" + const val ID2 = "idAlert2" + const val UID = "mock_uid" + const val UID2 = "mock_uid2" + val name = "test_name" + val name_update = "test_update_name" + val product = Product.PAD + val product_update = Product.TAMPON + val urgency = Urgency.LOW + val urgency_update = Urgency.MEDIUM + val createdAt = LocalDateTime(2022, 1, 1, 0, 0).toString() + val createdAt_update = LocalDateTime(2023, 2, 2, 1, 1).toString() + val location = "test_location" + val location_update = "test_update_location" + val message = "test_message" + val message_update = "test_update_message" + val status = Status.CREATED + val status_update = Status.PENDING + + val alert = + Alert( + id = ID, + uid = UID, + name = name, + product = product, + urgency = urgency, + createdAt = createdAt, + location = location, + message = message, + status = status) + val alertNullID = + Alert( + id = null, + uid = UID, + name = name, + product = product, + urgency = urgency, + createdAt = createdAt, + location = location, + message = message, + status = status) + val alertUpdated = + Alert( + id = ID, + uid = UID, + name = name_update, + product = product_update, + urgency = urgency_update, + createdAt = createdAt_update, + location = location_update, + message = message_update, + status = status_update) + val alertOther = + Alert( + id = ID2, + uid = UID2, + name = name, + product = product, + urgency = urgency, + createdAt = createdAt, + location = location, + message = message, + status = status) + } + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + // Create ViewModel with mocked AlertModelSupabase + viewModel = AlertViewModel(alertModelSupabase) + } + + @Test + fun createAlertSuccess() = runBlocking { + // Mock addAlert success behavior + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + // Mock getAllAlerts to verify it is called after successful addition + doAnswer { invocation -> + val onSuccess = invocation.getArgument<(List) -> Unit>(0) + onSuccess(listOf(alert)) // Return a list with our mock alert + null + } + .`when`(alertModelSupabase) + .getAllAlerts(any(), any()) + + viewModel.createAlert(alert) + + assertEquals(listOf(alert), viewModel.alerts.value) + } + + @Test + fun createAlertAddAlertFailure() = runBlocking { + // Mock addAlert success behavior + doAnswer { it.getArgument<(Exception) -> Unit>(2)(Exception("createAlert failure")) } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + viewModel.createAlert(alert) + assert(viewModel.alerts.value!!.isEmpty()) + } + + @Test + fun createAlertGetAlertFailure() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + doAnswer { it.getArgument<(Exception) -> Unit>(1)(Exception(" ")) } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + + viewModel.createAlert(alert) + + assert(viewModel.alerts.value!!.isEmpty()) + } + + @Test + fun deleteAlertSuccess() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + var calls = 0 + doAnswer { + if (calls == 0) { + it.getArgument<(List) -> Unit>(0)(listOf(alert)) + } else { + it.getArgument<(List) -> Unit>(0)(listOf()) + } + calls++ + } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .deleteAlertById(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + viewModel.createAlert(alert) + assert(viewModel.alerts.value!!.isNotEmpty()) + assertEquals(listOf(alert), viewModel.alerts.value) + + viewModel.deleteAlert(alert) + assert(viewModel.alerts.value!!.isEmpty()) + } + + @Test + fun deleteAlertNullIdFailure() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + doAnswer { it.getArgument<(List) -> Unit>(0)(listOf(alert)) } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + + viewModel.createAlert(alert) + viewModel.deleteAlert(alertNullID) + + assert(!viewModel.alerts.value!!.isEmpty()) + assertEquals(listOf(alert), viewModel.alerts.value) + } + + @Test + fun deleteAlertDeleteFailure() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + doAnswer { it.getArgument<(List) -> Unit>(0)(listOf(alert)) } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + + doAnswer { it.getArgument<(Exception) -> Unit>(2)(Exception("deleteAlertFailure")) } + .`when`(alertModelSupabase) + .deleteAlertById(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + viewModel.createAlert(alert) + assert(viewModel.alerts.value!!.isNotEmpty()) + assertEquals(listOf(alert), viewModel.alerts.value) + + viewModel.deleteAlert(alert) + assert(viewModel.alerts.value!!.isNotEmpty()) + assertEquals(listOf(alert), viewModel.alerts.value) + } + + @Test + fun getAlertSuccess() = runBlocking { + doAnswer { + assertEquals(ID, it.getArgument(0)) + it.getArgument<(Alert) -> Unit>(1)(alert) + } + .`when`(alertModelSupabase) + .getAlert(any(), any<(Alert) -> Unit>(), any<(Exception) -> Unit>()) + + val result = viewModel.getAlert(ID) + assertEquals(alert, result) + } + + @Test + fun getAlertGetAlertFailure() = runBlocking { + doAnswer { + assertEquals(ID, it.getArgument(0)) + it.getArgument<(Exception) -> Unit>(2)(Exception("Supabase Fails :(")) + } + .`when`(alertModelSupabase) + .getAlert(any(), any<(Alert) -> Unit>(), any<(Exception) -> Unit>()) + + val result = viewModel.getAlert(ID) + assertNull(result) + } + + @Test + fun getAlertsByUserSuccess() = runBlocking { + doAnswer { it.getArgument<(List) -> Unit>(1)(listOf(alert)) } + .`when`(alertModelSupabase) + .getAlertsFilteredBy( + any Unit>(), + any<(List) -> Unit>(), + any<(Exception) -> Unit>()) + + val result = viewModel.getAlertsByUser(UID) + assertEquals(listOf(alert), result) + } + + @Test + fun getAlertByUserFailure() = runBlocking { + doAnswer { it.getArgument<(Exception) -> Unit>(2)(Exception("Supabase Fails :(")) } + .`when`(alertModelSupabase) + .getAlertsFilteredBy( + any Unit>(), + any<(List) -> Unit>(), + any<(Exception) -> Unit>()) + + val result = viewModel.getAlertsByUser(UID) + assertNull(result) + } + + @Test + fun updateAlertSuccess() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .updateAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + var count = 0 + doAnswer { + if (count < 1) { + it.getArgument<(List) -> Unit>(0)(listOf(alert)) + } else { + it.getArgument<(List) -> Unit>(0)(listOf(alertUpdated)) + } + count++ + } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + + viewModel.createAlert(alert) + assertEquals(listOf(alert), viewModel.alerts.value) + + viewModel.updateAlert(alertUpdated) + assertEquals(listOf(alertUpdated), viewModel.alerts.value) + } + + @Test + fun updateAlertFailure() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + doAnswer { it.getArgument<(List) -> Unit>(0)(listOf(alert)) } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + + doAnswer { it.getArgument<(Exception) -> Unit>(2)(Exception("Supabase fail")) } + .`when`(alertModelSupabase) + .updateAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + + viewModel.createAlert(alert) + assertEquals(listOf(alert), viewModel.alerts.value) + + viewModel.updateAlert(alertUpdated) + assertEquals(listOf(alert), viewModel.alerts.value) + } + + @Test + fun getPalAlertsSuccess() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + var count = 0 + doAnswer { + if (count == 0) { + it.getArgument<(List) -> Unit>(0)(listOf(alert)) + } else { + it.getArgument<(List) -> Unit>(0)(listOf(alert, alertOther)) + } + count++ + } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + assertEquals(listOf(), viewModel.alerts.value) + viewModel.createAlert(alert) + assertEquals(listOf(alert), viewModel.alerts.value) + viewModel.createAlert(alertOther) + assertEquals(listOf(alert, alertOther), viewModel.alerts.value) + + val result: List? = viewModel.getPalAlerts(alert.uid) + + assertNotNull(result) + assert(result!!.isNotEmpty()) + assertEquals(listOf(alertOther), result) + } + + @Test + fun getPalAlertsFailure() = runBlocking { + doAnswer { it.getArgument<() -> Unit>(1)() } + .`when`(alertModelSupabase) + .addAlert(any(), any<() -> Unit>(), any<(Exception) -> Unit>()) + var count = 0 + doAnswer { + if (count == 0) { + it.getArgument<(List) -> Unit>(0)(listOf(alert)) + } else if (count == 1) { + it.getArgument<(List) -> Unit>(0)(listOf(alert, alertOther)) + } else { + it.getArgument<(Exception) -> Unit>(1)(Exception("Supabase fail :(")) + } + count++ + } + .`when`(alertModelSupabase) + .getAllAlerts(any<(List) -> Unit>(), any<(Exception) -> Unit>()) + assertEquals(listOf(), viewModel.alerts.value) + viewModel.createAlert(alert) + assertEquals(listOf(alert), viewModel.alerts.value) + viewModel.createAlert(alertOther) + assertEquals(listOf(alert, alertOther), viewModel.alerts.value) + + val result: List? = viewModel.getPalAlerts(alert.uid) + assertNull(result) + } +}