diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt index 075f236b..9fe5b8c0 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt @@ -64,7 +64,7 @@ class AddActivityScreenTest { "description", Location(0.0, 0.0, Timestamp(0, 0), "location"), Timestamp(0, 0), - mapOf()) + emptyList()) @get:Rule val composeTestRule = createComposeRule() diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt index fd1281f7..b248cd98 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt @@ -42,7 +42,7 @@ class EditActivityScreenTest { "description", Location(0.0, 0.0, Timestamp(0, 0), "location"), Timestamp(0, 0), - mapOf()) + emptyList()) @get:Rule val composeTestRule = createComposeRule() diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TravelActivityScreen.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TravelActivityScreen.kt index 957fcd96..1832d6f4 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TravelActivityScreen.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TravelActivityScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.ui.test.performClick import com.github.se.travelpouch.model.activity.Activity import com.github.se.travelpouch.model.activity.ActivityRepository import com.github.se.travelpouch.model.activity.ActivityViewModel +import com.github.se.travelpouch.model.documents.DocumentRepository +import com.github.se.travelpouch.model.documents.DocumentViewModel import com.github.se.travelpouch.model.travels.Location import com.github.se.travelpouch.ui.navigation.NavigationActions import com.google.firebase.Timestamp @@ -26,6 +28,8 @@ class TravelActivityScreenCopy { private lateinit var mockActivityRepositoryFirebase: ActivityRepository private lateinit var mockActivityModelView: ActivityViewModel private lateinit var navigationActions: NavigationActions + private lateinit var mockDocumentViewModel: DocumentViewModel + private lateinit var mockDocumentRepository: DocumentRepository val activites_test = listOf( @@ -35,14 +39,14 @@ class TravelActivityScreenCopy { "description1", Location(0.0, 0.0, Timestamp(0, 0), "lcoation1"), Timestamp(0, 0), - mapOf()), + emptyList()), Activity( "2", "title2", "description2", Location(0.0, 0.0, Timestamp(0, 0), "lcoation2"), Timestamp(0, 0), - mapOf())) + emptyList())) @get:Rule val composeTestRule = createComposeRule() @@ -51,12 +55,17 @@ class TravelActivityScreenCopy { navigationActions = mock(NavigationActions::class.java) mockActivityRepositoryFirebase = mock(ActivityRepository::class.java) mockActivityModelView = ActivityViewModel(mockActivityRepositoryFirebase) + mockDocumentRepository = mock() + mockDocumentViewModel = DocumentViewModel(mockDocumentRepository, mock(), mock()) } @Test fun verifiesEverythingIsDisplayed() { composeTestRule.setContent { - TravelActivitiesScreen(navigationActions, activityModelView = mockActivityModelView) + TravelActivitiesScreen( + navigationActions, + activityModelView = mockActivityModelView, + documentViewModel = mockDocumentViewModel) } `when`(mockActivityRepositoryFirebase.getAllActivities(any(), any())).then { @@ -75,7 +84,10 @@ class TravelActivityScreenCopy { @Test fun verifiesEmptyPromptWhenEmptyList() { composeTestRule.setContent { - TravelActivitiesScreen(navigationActions, activityModelView = mockActivityModelView) + TravelActivitiesScreen( + navigationActions, + activityModelView = mockActivityModelView, + documentViewModel = mockDocumentViewModel) } `when`(mockActivityRepositoryFirebase.getAllActivities(any(), any())).then { @@ -90,46 +102,15 @@ class TravelActivityScreenCopy { @Test fun verifyActivityCardWorksCorrectly() { val activity = activites_test[0] - val images = - listOf( - "https://img.yumpu.com/30185842/1/500x640/afps-attestation-de-formation-aux-premiers-secours-programme-.jpg", - "https://wallpapercrafter.com/desktop6/1606440-architecture-buildings-city-downtown-finance-financial.jpg", - "https://assets.entrepreneur.com/content/3x2/2000/20151023204134-poker-game-gambling-gamble-cards-money-chips-game.jpeg") - composeTestRule.setContent { ActivityItem(activity, {}, LocalContext.current, images) } + + composeTestRule.setContent { + ActivityItem(activity, {}, LocalContext.current, mockDocumentViewModel) + } composeTestRule.waitForIdle() composeTestRule.onNodeWithTag("activityItem").assertIsDisplayed() composeTestRule.onNodeWithTag("activityItem").assertTextContains(activity.title) composeTestRule.onNodeWithTag("activityItem").assertTextContains(activity.location.name) composeTestRule.onNodeWithTag("activityItem").assertTextContains("1/1/1970") - composeTestRule.onNodeWithTag("extraDocumentButton").assertIsDisplayed().performClick() - } - - @Test - fun verify1ImageActivity() { - val activity = activites_test[0] - val images = - listOf( - "https://img.yumpu.com/30185842/1/500x640/afps-attestation-de-formation-aux-premiers-secours-programme-.jpg", - "https://wallpapercrafter.com/desktop6/1606440-architecture-buildings-city-downtown-finance-financial.jpg", - "https://assets.entrepreneur.com/content/3x2/2000/20151023204134-poker-game-gambling-gamble-cards-money-chips-game.jpeg") - composeTestRule.setContent { - ActivityItem(activity, {}, LocalContext.current, listOf(images[0])) - } - composeTestRule.waitForIdle() - } - - @Test - fun verify2ImagesActivity() { - val activity = activites_test[0] - val images = - listOf( - "https://img.yumpu.com/30185842/1/500x640/afps-attestation-de-formation-aux-premiers-secours-programme-.jpg", - "https://wallpapercrafter.com/desktop6/1606440-architecture-buildings-city-downtown-finance-financial.jpg", - "https://assets.entrepreneur.com/content/3x2/2000/20151023204134-poker-game-gambling-gamble-cards-money-chips-game.jpeg") - composeTestRule.setContent { - ActivityItem(activity, {}, LocalContext.current, listOf(images[0], images[1])) - } - composeTestRule.waitForIdle() } @Test @@ -137,10 +118,6 @@ class TravelActivityScreenCopy { composeTestRule.setContent { DefaultErrorUI() } } - fun runAsyncLoadingSpinner() { - composeTestRule.setContent { AdvancedImageDisplayWithEffects("https://epic.gamer.huh") } - } - @Test fun verifyBannerIsDisplayedCorrectly() { val nowSeconds = Timestamp.now().seconds @@ -152,14 +129,14 @@ class TravelActivityScreenCopy { "description1", Location(0.0, 0.0, Timestamp(0, 0), "lcoation1"), Timestamp(nowSeconds + 3600L, 0), - mapOf()), + emptyList()), Activity( "2", "title2", "description2", Location(0.0, 0.0, Timestamp(0, 0), "lcoation2"), Timestamp(nowSeconds + 3600L, 0), - mapOf())) + emptyList())) composeTestRule.setContent { NextActivitiesBanner(activitiesNow, {}) } composeTestRule.onNodeWithTag("NextActivitiesBannerBox").assertIsDisplayed() @@ -190,14 +167,14 @@ class TravelActivityScreenCopy { "description1", Location(0.0, 0.0, Timestamp(0, 0), "location1"), Timestamp(nowSeconds - 100_000L, 0), - mapOf()), + emptyList()), Activity( "2", "title2", "description2", Location(0.0, 0.0, Timestamp(0, 0), "location2"), Timestamp(nowSeconds + 100_000L, 0), - mapOf())) + emptyList())) composeTestRule.setContent { NextActivitiesBanner(activitiesOutOfDate, {}) } composeTestRule.onNodeWithTag("NextActivitiesBannerBox").assertDoesNotExist() diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/map/ActivitiesMapScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/map/ActivitiesMapScreenTest.kt index 0cc54f25..e9d52ed8 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/map/ActivitiesMapScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/map/ActivitiesMapScreenTest.kt @@ -58,21 +58,21 @@ class ActivitiesMapScreenTest { description = "Monthly team meeting to discuss project progress.", location = Location(48.8566, 2.3522, Timestamp.now(), "Paris"), date = createTimestamp("20/12/2024 12:00"), - documentsNeeded = mapOf("Agenda" to 1, "Meeting Notes" to 2)), + documentsNeeded = emptyList()), Activity( uid = "2", title = "Client Presentation", description = "Presentation to showcase the project to the client.", location = Location(49.8566, 2.3522, Timestamp.now(), "Paris"), date = createTimestamp("21/12/2024 13:00"), - documentsNeeded = null), + documentsNeeded = emptyList()), Activity( uid = "3", title = "Workshop", description = "Workshop on team building and skill development.", location = Location(49.02, 2.5, Timestamp.now(), "Paris"), date = createTimestamp("23/12/2024 14:00"), - documentsNeeded = mapOf("Workshop Material" to 1))) + documentsNeeded = emptyList())) private val mockLeg = Leg( diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/documents/DocumentPreviewTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/documents/DocumentPreviewTest.kt index 102542d1..1af0b09a 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/documents/DocumentPreviewTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/documents/DocumentPreviewTest.kt @@ -5,10 +5,13 @@ import android.content.Context import android.provider.DocumentsContract import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertTextContains import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.core.net.toUri import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -16,12 +19,16 @@ import androidx.datastore.preferences.core.preferencesOf import androidx.datastore.preferences.core.stringPreferencesKey import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.github.se.travelpouch.di.AppModule +import com.github.se.travelpouch.model.activity.Activity +import com.github.se.travelpouch.model.activity.ActivityRepository +import com.github.se.travelpouch.model.activity.ActivityViewModel import com.github.se.travelpouch.model.documents.DocumentContainer import com.github.se.travelpouch.model.documents.DocumentFileFormat import com.github.se.travelpouch.model.documents.DocumentRepository import com.github.se.travelpouch.model.documents.DocumentViewModel import com.github.se.travelpouch.model.documents.DocumentVisibility import com.github.se.travelpouch.model.documents.DocumentsManager +import com.github.se.travelpouch.model.travels.Location import com.github.se.travelpouch.ui.navigation.NavigationActions import com.google.firebase.FirebaseApp import com.google.firebase.Timestamp @@ -36,7 +43,9 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mockito.mock import org.mockito.Mockito.`when` +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.spy +import org.mockito.kotlin.verify @HiltAndroidTest @UninstallModules(AppModule::class) @@ -51,10 +60,16 @@ class DocumentPreviewTest { private lateinit var mockDataStore: DataStore private lateinit var file: File + private lateinit var mockActivityRepository: ActivityRepository + private lateinit var mockActivityViewModel: ActivityViewModel + @get:Rule val composeTestRule = createComposeRule() @Before fun setUp() { + mockActivityRepository = mock() + mockActivityViewModel = ActivityViewModel(mockActivityRepository) + mockDocumentReference = mock(DocumentReference::class.java) `when`(mockDocumentReference.id).thenReturn("ref_id") @@ -106,7 +121,9 @@ class DocumentPreviewTest { @Test fun testsEverythingIsDisplayed() { - composeTestRule.setContent { DocumentPreview(mockDocumentViewModel, navigationActions) } + composeTestRule.setContent { + DocumentPreview(mockDocumentViewModel, navigationActions, mockActivityViewModel) + } composeTestRule.onNodeWithTag("documentPreviewScreen").assertIsDisplayed() composeTestRule.onNodeWithTag("documentTitleTopBarApp").assertIsDisplayed() @@ -114,9 +131,76 @@ class DocumentPreviewTest { composeTestRule.onNodeWithTag("goBackButton").assertIsDisplayed() composeTestRule.onNodeWithTag("deleteButton").assertIsDisplayed() + composeTestRule.onNodeWithTag("linkingButton").assertIsDisplayed() composeTestRule .onNodeWithTag("documentTitle", useUnmergedTree = true) .assertTextContains("Document ID: ref_id") composeTestRule.waitUntil(1000) { composeTestRule.onNodeWithTag("document").isDisplayed() } } + + @Test + fun testLinkingNotavailableWhenNoActivities() { + `when`(mockActivityRepository.getAllActivities(anyOrNull(), anyOrNull())).then { + it.getArgument<(List) -> Unit>(0)(emptyList()) + } + mockActivityViewModel.getAllActivities() + + composeTestRule.setContent { + DocumentPreview(mockDocumentViewModel, navigationActions, mockActivityViewModel) + } + + composeTestRule.onNodeWithTag("linkingButton").assertIsDisplayed().performClick() + composeTestRule.onNodeWithTag("activitiesDialog").assertIsNotDisplayed() + } + + @Test + fun testLinkingActivityWhenActivityAvailable() { + val activity = + Activity( + "qwertzuiopasdfghjkly", + "titleAc", + "descriptionAc", + Location(0.0, 0.0, Timestamp.now(), "nameAc"), + Timestamp(0, 0), + emptyList()) + + `when`(mockActivityRepository.getAllActivities(anyOrNull(), anyOrNull())).then { + it.getArgument<(List) -> Unit>(0)(listOf(activity)) + } + mockActivityViewModel.getAllActivities() + + composeTestRule.setContent { + DocumentPreview(mockDocumentViewModel, navigationActions, mockActivityViewModel) + } + + composeTestRule.onNodeWithTag("linkingButton").assertIsDisplayed().performClick() + composeTestRule.onNodeWithTag("activitiesDialog").assertIsDisplayed() + composeTestRule.onNodeWithText("Activity to link the image to").assertIsDisplayed() + composeTestRule.onNodeWithTag("activitiesList").assertIsDisplayed() + composeTestRule.onNodeWithTag("activityItem_qwertzuiopasdfghjkly").assertIsDisplayed() + composeTestRule.onNodeWithTag("activityItem_qwertzuiopasdfghjkly").assertTextContains("titleAc") + composeTestRule.onNodeWithTag("activityItem_qwertzuiopasdfghjkly").assertTextContains("nameAc") + composeTestRule + .onNodeWithTag("activityItem_qwertzuiopasdfghjkly") + .assertTextContains("1/1/1970") + + composeTestRule.onNodeWithTag("activityItem_qwertzuiopasdfghjkly").performClick() + composeTestRule.onNodeWithTag("activitiesDialog").assertIsNotDisplayed() + composeTestRule.onNodeWithText("Activity to link the image to").assertIsNotDisplayed() + composeTestRule.onNodeWithTag("activitiesList").assertIsNotDisplayed() + composeTestRule.onNodeWithTag("activityItem_qwertzuiopasdfghjkly").assertIsNotDisplayed() + + verify(mockActivityRepository).updateActivity(anyOrNull(), anyOrNull(), anyOrNull()) + } + + @Test + fun testDeleteCallsDelete() { + composeTestRule.setContent { + DocumentPreview(mockDocumentViewModel, navigationActions, mockActivityViewModel) + } + + composeTestRule.onNodeWithTag("deleteButton").performClick() + verify(mockDocumentRepository) + .deleteDocumentById(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } } diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt index ea07aed9..18f6b7d0 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt @@ -70,7 +70,7 @@ class CalendarScreenTest { date = Timestamp(today), description = "This is a mock activity for today.", location = Location(0.0, 0.0, Timestamp(0, 0), "location"), - documentsNeeded = mapOf()) + documentsNeeded = emptyList()) val listOfActivities = listOf(activityToday) diff --git a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt index 7ede59e4..06b325e2 100644 --- a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt @@ -160,7 +160,7 @@ class MainActivity : ComponentActivity() { }) } composable(Screen.DOCUMENT_PREVIEW) { - DocumentPreview(documentViewModel, navigationActions) + DocumentPreview(documentViewModel, navigationActions, activityModelView) } composable(Screen.TIMELINE) { TimelineScreen(eventsViewModel, navigationActions) } diff --git a/app/src/main/java/com/github/se/travelpouch/model/activity/Activity.kt b/app/src/main/java/com/github/se/travelpouch/model/activity/Activity.kt index c4d0fb78..728cb2c6 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/activity/Activity.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/activity/Activity.kt @@ -1,6 +1,7 @@ // Portions of this code were generated and or inspired by the help of GitHub Copilot or Chatgpt package com.github.se.travelpouch.model.activity +import com.github.se.travelpouch.model.documents.DocumentContainer import com.github.se.travelpouch.model.travels.Location import com.google.firebase.Timestamp @@ -13,8 +14,8 @@ import com.google.firebase.Timestamp * @property description (String) : the description of the travel * @property location (Location) : the location where the activity takes place * @property date (Timestamp) : the date when the activity will occur - * @property documentsNeeded (Map?) : the list of documents needed for the activity. If - * no document is needed the map is null + * @property documentsNeeded (List) : the list of documents needed for the + * activity. */ data class Activity( val uid: String, @@ -22,5 +23,5 @@ data class Activity( val description: String, val location: Location, val date: Timestamp, - val documentsNeeded: Map? + val documentsNeeded: List = emptyList() ) diff --git a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt index d8e223a4..c6100b4d 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt @@ -3,6 +3,9 @@ package com.github.se.travelpouch.model.activity import android.util.Log import com.github.se.travelpouch.model.FirebasePaths +import com.github.se.travelpouch.model.documents.DocumentContainer +import com.github.se.travelpouch.model.documents.DocumentFileFormat +import com.github.se.travelpouch.model.documents.DocumentVisibility import com.github.se.travelpouch.model.events.Event import com.github.se.travelpouch.model.events.EventType import com.github.se.travelpouch.model.travels.Location @@ -179,9 +182,23 @@ class ActivityRepositoryFirebase(private val db: FirebaseFirestore) : ActivityRe val title = document.getString("title") val description = document.getString("description") val date = document.getTimestamp("date") - val documentsNeededData = document["documentsNeeded"] as? Map<*, *> + val documentsNeededData = document["documentsNeeded"] as? List> val documentsNeeded = - documentsNeededData?.map { (key, value) -> key as String to value as Int }?.toMap() + documentsNeededData + ?.map { + DocumentContainer( + ref = it["ref"] as DocumentReference, + travelRef = it["travelRef"] as DocumentReference, + activityRef = it["activityRef"] as DocumentReference?, + title = it["title"] as String, + fileFormat = DocumentFileFormat.valueOf(it["fileFormat"] as String), + fileSize = it["fileSize"] as Long, + addedByEmail = it["addedByEmail"] as String?, + addedByUser = it["addedByUser"] as DocumentReference?, + addedAt = it["addedAt"] as Timestamp, + visibility = DocumentVisibility.valueOf(it["visibility"] as String)) + } + ?.toList() ?: emptyList() val locationData = document.get("location") as? Map<*, *> val location = locationData?.let { diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt index f61730bd..16ea6cb1 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt @@ -3,6 +3,7 @@ package com.github.se.travelpouch.model.documents import android.util.Log import com.github.se.travelpouch.model.FirebasePaths +import com.github.se.travelpouch.model.activity.Activity import com.google.android.gms.common.util.Base64Utils import com.google.firebase.Timestamp import com.google.firebase.auth.FirebaseAuth @@ -17,7 +18,12 @@ interface DocumentRepository { fun getDocuments(onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit) - fun deleteDocumentById(id: String, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) + fun deleteDocumentById( + document: DocumentContainer, + listOfActivities: List, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit + ) fun uploadDocument( travelId: String, @@ -76,25 +82,50 @@ class DocumentRepositoryFirestore( /** * Deletes a document from the Firestore database. * - * @param id The id of the document to be deleted. + * @param document The document to be deleted. + * @param listOfActivities The list of activities the document is linked to * @param onSuccess Callback function to be called when the document is deleted successfully. * @param onFailure Callback function to be called when an error occurs. */ override fun deleteDocumentById( - id: String, + document: DocumentContainer, + listOfActivities: List, onSuccess: () -> Unit, onFailure: (Exception) -> Unit ) { - db.collection(collectionPath).document(id).delete().addOnCompleteListener { task -> - if (task.isSuccessful) { - onSuccess() - } else { - task.exception?.let { e -> - Log.e("DocumentRepositoryFirestore", "Error deleting document", e) - onFailure(e) + db.runTransaction { + val referenceDocument = document.ref + + val referenceToActivitiesCollection = + db.collection( + FirebasePaths.constructPath( + collectionPath.dropLast(FirebasePaths.documents.length + 1), + FirebasePaths.activities)) + + it.delete(referenceDocument) + + val listActivitiesUpdated = + listOfActivities.map { ac -> + val documentsNeeded = ac.documentsNeeded.toMutableList() + documentsNeeded.remove(document) + ac.copy(documentsNeeded = documentsNeeded.toList()) + } + + for (activity in listActivitiesUpdated) { + val activityReference = referenceToActivitiesCollection.document(activity.uid) + it.set(activityReference, activity) + } + } + .addOnCompleteListener { task -> + if (task.isSuccessful) { + onSuccess() + } else { + task.exception?.let { e -> + Log.e("DocumentRepositoryFirestore", "Error deleting document", e) + onFailure(e) + } + } } - } - } } override fun uploadDocument( diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt index 202e80ea..a9bb557e 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt @@ -12,6 +12,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel +import com.github.se.travelpouch.model.activity.Activity import com.github.se.travelpouch.model.travels.TravelContainer import dagger.hilt.android.lifecycle.HiltViewModel import java.io.ByteArrayOutputStream @@ -98,13 +99,15 @@ constructor( } /** - * Deletes a Document by its ID. + * Deletes a Document. * - * @param id The ID of the Document to be deleted. + * @param document The Document to be deleted. + * @param activityList The list of activities the document is linked to */ - fun deleteDocumentById(id: String) { + fun deleteDocumentById(document: DocumentContainer, activityList: List) { repository.deleteDocumentById( - id, + document, + activityList, onSuccess = { getDocuments() }, onFailure = { Log.e("DocumentsViewModel", "Failed to delete Document", it) }) } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt index 5105df2e..50434d6d 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt @@ -184,7 +184,7 @@ fun AddActivityScreen( description, selectedLocation ?: Location(0.0, 0.0, finalDate, "Unknown"), finalDate, - mapOf()) + emptyList()) activityModelView.addActivity( activity, context, eventViewModel.getNewDocumentReference()) diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/EditActivity.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/EditActivity.kt index 797e35df..13b203d3 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/EditActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/EditActivity.kt @@ -233,7 +233,7 @@ fun EditActivity( description, newLocation, finalDate, - mapOf()) + selectedActivity.value!!.documentsNeeded) activityViewModel.updateActivity(activity, context) navigationActions.navigateTo(Screen.SWIPER) diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/TravelActivity.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/TravelActivity.kt index abf032bc..15b008b0 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/TravelActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/TravelActivity.kt @@ -1,7 +1,7 @@ // Portions of this code were generated and or inspired by the help of GitHub Copilot or Chatgpt package com.github.se.travelpouch.ui.dashboard -import android.widget.Toast +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Card import androidx.compose.material3.CardColors import androidx.compose.material3.CardDefaults @@ -23,14 +22,16 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold 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 import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -41,9 +42,10 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest import com.github.se.travelpouch.model.activity.Activity import com.github.se.travelpouch.model.activity.ActivityViewModel +import com.github.se.travelpouch.model.documents.DocumentContainer +import com.github.se.travelpouch.model.documents.DocumentViewModel import com.github.se.travelpouch.ui.navigation.NavigationActions import com.github.se.travelpouch.ui.navigation.Screen import java.util.Calendar @@ -62,7 +64,8 @@ private const val A4_ASPECT_RATIO = 1f / 1.414f @Composable fun TravelActivitiesScreen( navigationActions: NavigationActions, - activityModelView: ActivityViewModel + activityModelView: ActivityViewModel, + documentViewModel: DocumentViewModel ) { activityModelView.getAllActivities() @@ -136,7 +139,7 @@ fun TravelActivitiesScreen( navigationActions.navigateTo(Screen.EDIT_ACTIVITY) }, LocalContext.current, - images) + documentViewModel) } } } @@ -165,7 +168,7 @@ fun ActivityItem( activity: Activity, onClick: () -> Unit = {}, context: android.content.Context, - images: List + documentViewModel: DocumentViewModel ) { val calendar = GregorianCalendar().apply { time = activity.date.toDate() } // we hardcode for the moment placeholder images @@ -194,9 +197,9 @@ fun ActivityItem( fontWeight = FontWeight.Light) // Handling image display logic - if (images.isEmpty()) { + if (activity.documentsNeeded.isEmpty()) { // No images to show, do nothing - } else if (images.size == 1) { + } else if (activity.documentsNeeded.size == 1) { // Single image Box( modifier = @@ -205,16 +208,17 @@ fun ActivityItem( A4_ASPECT_RATIO) // Maintain A4 aspect ratio (width / height ~ 1:1.414) .background(Color.Transparent)) { AdvancedImageDisplayWithEffects( - imageUrl = images[0], + documentViewModel, + activity.documentsNeeded[0], loadingContent = { DefaultLoadingUI() }, errorContent = { DefaultErrorUI() }) } - } else if (images.size == 2) { + } else { // Two images - display side by side with space in between Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { - images.take(2).forEach { imageUrl -> + activity.documentsNeeded.take(2).forEach { document -> Box( modifier = Modifier.weight(1f) @@ -222,57 +226,13 @@ fun ActivityItem( A4_ASPECT_RATIO) // Use the same A4 aspect ratio for both images .background(Color.Transparent)) { AdvancedImageDisplayWithEffects( - imageUrl = imageUrl, + documentViewModel, + document, loadingContent = { DefaultLoadingUI() }, errorContent = { DefaultErrorUI() }) } } } - } else if (images.size >= 3) { - // Three or more images - show the first two with a button above them - Column(modifier = Modifier.fillMaxWidth()) { - Box { // Box to layer - // Display the first two images inside a Row - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp)) { - images.take(2).forEach { imageUrl -> - Box( - modifier = - Modifier.weight(1f) // Ensure the images take equal space - .aspectRatio(A4_ASPECT_RATIO) // Maintain A4 aspect ratio - .background(Color.Transparent)) { - AdvancedImageDisplayWithEffects( - imageUrl = imageUrl, - loadingContent = { DefaultLoadingUI() }, - errorContent = { DefaultErrorUI() }) - } - } - } - - // More options button on top of the second image - IconButton( - onClick = { - Toast.makeText( - context, "Placeholder for document view of activity", Toast.LENGTH_LONG) - .show() - }, - modifier = - Modifier.align(Alignment.BottomEnd) // Position the button on the bottom right - .background( - Color.Gray, - shape = - androidx.compose.foundation.shape.RoundedCornerShape( - 10)) // Rounded background - .testTag("extraDocumentButton") // Add padding to the button - ) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = "More options", - tint = Color.White) - } - } - } } } } @@ -290,12 +250,20 @@ fun ActivityItem( */ @Composable fun AdvancedImageDisplayWithEffects( - imageUrl: String, + documentViewModel: DocumentViewModel, + documentContainer: DocumentContainer, loadingContent: @Composable () -> Unit = { DefaultLoadingUI() }, errorContent: @Composable () -> Unit = { DefaultErrorUI() } ) { + var thumbnailUri by remember { mutableStateOf(null) } + LaunchedEffect(documentContainer) { + documentViewModel.getDocumentThumbnail(documentContainer, 150) + } + thumbnailUri = documentViewModel.thumbnailUris["${documentContainer.ref.id}-${150}"] + SubcomposeAsyncImage( - model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(), + model = + thumbnailUri, // ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(), contentDescription = "Advanced Image with Effects", modifier = Modifier.fillMaxWidth(), contentScale = ContentScale.Fit, diff --git a/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentPreview.kt b/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentPreview.kt index ddcd50aa..da040fed 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentPreview.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentPreview.kt @@ -1,20 +1,27 @@ // Portions of this code were generated and or inspired by the help of GitHub Copilot or Chatgpt package com.github.se.travelpouch.ui.documents +import android.annotation.SuppressLint import android.content.Intent import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.filled.AddLink import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -26,7 +33,10 @@ 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 +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -34,9 +44,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.documentfile.provider.DocumentFile import coil.compose.rememberAsyncImagePainter +import com.github.se.travelpouch.model.activity.ActivityViewModel import com.github.se.travelpouch.model.documents.DocumentContainer import com.github.se.travelpouch.model.documents.DocumentFileFormat import com.github.se.travelpouch.model.documents.DocumentViewModel @@ -44,6 +57,8 @@ import com.github.se.travelpouch.ui.navigation.NavigationActions import com.rizzi.bouquet.ResourceType import com.rizzi.bouquet.VerticalPDFReader import com.rizzi.bouquet.rememberVerticalPdfReaderState +import java.util.Calendar +import java.util.GregorianCalendar import kotlinx.coroutines.launch /** @@ -51,9 +66,16 @@ import kotlinx.coroutines.launch * * @param documentViewModel the document view model with the current document set as selected. */ +@SuppressLint("StateFlowValueCalledInComposition") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DocumentPreview(documentViewModel: DocumentViewModel, navigationActions: NavigationActions) { +fun DocumentPreview( + documentViewModel: DocumentViewModel, + navigationActions: NavigationActions, + activityViewModel: ActivityViewModel +) { + var openDialog by remember { mutableStateOf(false) } + val documentContainer: DocumentContainer = documentViewModel.selectedDocument.collectAsState().value!! val uri = documentViewModel.documentUri.value @@ -110,12 +132,33 @@ fun DocumentPreview(documentViewModel: DocumentViewModel, navigationActions: Nav actions = { IconButton( onClick = { - documentViewModel.deleteDocumentById(documentContainer.ref.id) + val activitiesLinkedToDocument = + activityViewModel.activities.value.filter { + it.documentsNeeded.contains(documentContainer) + } + documentViewModel.deleteDocumentById( + documentContainer, activitiesLinkedToDocument) navigationActions.goBack() }, modifier = Modifier.testTag("deleteButton")) { Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete Document") } + + IconButton( + onClick = { + if (activityViewModel.activities.value.isEmpty()) { + Toast.makeText( + context, + "Cannot link image if there are no activities", + Toast.LENGTH_LONG) + .show() + } else { + openDialog = true + } + }, + modifier = Modifier.testTag("linkingButton")) { + Icon(imageVector = Icons.Default.AddLink, contentDescription = null) + } }) }, ) { paddingValue -> @@ -146,5 +189,72 @@ fun DocumentPreview(documentViewModel: DocumentViewModel, navigationActions: Nav } } } + + if (openDialog) { + Dialog( + onDismissRequest = { openDialog = false }, + ) { + Column( + modifier = + Modifier.fillMaxWidth(0.9f) + .background(MaterialTheme.colorScheme.background) + .testTag("activitiesDialog"), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly) { + Text( + "Activity to link the image to", + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(8.dp)) + + LazyColumn( + modifier = + Modifier.fillMaxWidth(1f) + .padding(horizontal = 8.dp) + .testTag("activitiesList"), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(vertical = 8.dp)) { + items(activityViewModel.activities.value.size) { i -> + val activity = activityViewModel.activities.value[i] + val calendar = GregorianCalendar().apply { time = activity.date.toDate() } + + Card( + modifier = Modifier.testTag("activityItem_${activity.uid}").fillMaxSize(), + elevation = CardDefaults.cardElevation(defaultElevation = 3.dp), + onClick = { + if (!activity.documentsNeeded.contains(documentContainer)) { + val documentNeeded = activity.documentsNeeded.toMutableList() + documentNeeded.add(documentContainer) + val newActivity = + activity.copy(documentsNeeded = documentNeeded.toList()) + activityViewModel.updateActivity(newActivity, context) + openDialog = false + } else { + Toast.makeText( + context, + "You already linked this document to this activity", + Toast.LENGTH_LONG) + .show() + } + }) { + Column(modifier = Modifier.padding(8.dp)) { + Text( + activity.title, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyLarge) + Text( + activity.location.name, + style = MaterialTheme.typography.bodyMedium) + Text( + "${calendar.get(Calendar.DAY_OF_MONTH)}/${calendar.get(Calendar.MONTH) + 1}/${calendar.get( + Calendar.YEAR)}", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Light) + } + } + } + } + } + } + } } } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt b/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt index 82fca44d..2a0220f0 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt @@ -125,7 +125,7 @@ fun SwipePager( }) { pd -> HorizontalPager(state = pagerState, modifier = Modifier.padding(pd)) { pageIndex -> when (pageIndex) { - 0 -> TravelActivitiesScreen(navigationActions, activityViewModel) + 0 -> TravelActivitiesScreen(navigationActions, activityViewModel, documentViewModel) 1 -> CalendarScreen(calendarViewModel, navigationActions) 2 -> DocumentListScreen( diff --git a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt index e60c5ed9..7e75df65 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt @@ -29,7 +29,7 @@ class ActivityModelViewUnitTest { "description", Location(0.0, 0.0, Timestamp(0, 0), "location"), Timestamp(0, 0), - mapOf()) + emptyList()) val activity2 = Activity( @@ -38,7 +38,7 @@ class ActivityModelViewUnitTest { "description2", Location(0.0, 0.0, Timestamp(0, 0), "location2"), Timestamp(50, 0), // Earlier timestamp - mapOf()) + emptyList()) val activity3 = Activity( @@ -47,7 +47,7 @@ class ActivityModelViewUnitTest { "description3", Location(0.0, 0.0, Timestamp(0, 0), "location3"), Timestamp(150, 0), // Latest timestamp - mapOf()) + emptyList()) @Before fun setUp() { diff --git a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt index 0cd09a25..7191b96b 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt @@ -3,6 +3,9 @@ package com.github.se.travelpouch.model.activity import android.os.Looper import androidx.test.core.app.ApplicationProvider +import com.github.se.travelpouch.model.documents.DocumentContainer +import com.github.se.travelpouch.model.documents.DocumentFileFormat +import com.github.se.travelpouch.model.documents.DocumentVisibility import com.github.se.travelpouch.model.travels.Location import com.google.android.gms.tasks.OnSuccessListener import com.google.android.gms.tasks.Task @@ -43,6 +46,9 @@ class ActivityRepositoryUnitTest { @Mock private lateinit var mockDocumentSnapshot: DocumentSnapshot @Mock private lateinit var mockToDoQuerySnapshot: QuerySnapshot + @Mock private lateinit var mockDocumentDocumentReference: DocumentReference + @Mock private lateinit var mockTravelReference: DocumentReference + private lateinit var activityRepositoryFirestore: ActivityRepositoryFirebase val activity = @@ -52,10 +58,13 @@ class ActivityRepositoryUnitTest { "activityDescription", Location(0.0, 0.0, Timestamp(0, 0), "location"), Timestamp(0, 0), - null) + listOf()) @Before fun setUp() { + mockTravelReference = mock() + mockDocumentDocumentReference = mock() + MockitoAnnotations.openMocks(this) // Initialize Firebase if necessary @@ -77,7 +86,20 @@ class ActivityRepositoryUnitTest { "longitude" to activity.location.longitude, "name" to activity.location.name, "insertTime" to activity.location.insertTime)) - `when`(mockDocumentSnapshot.get("documentsNeeded")).thenReturn(null) + `when`(mockDocumentSnapshot.get("documentsNeeded")) + .thenReturn( + listOf( + mapOf( + "ref" to mockDocumentDocumentReference, + "travelRef" to mockTravelReference, + "activityRef" to null, + "title" to "titleDoc", + "fileFormat" to "JPEG", + "fileSize" to 0L, + "addedByEmail" to null, + "addedByUser" to null, + "addedAt" to Timestamp(0, 0), + "visibility" to "ME"))) `when`(mockFirestore.collection(any())).thenReturn(mockCollectionReference) `when`(mockCollectionReference.document(any())).thenReturn(mockDocumentReference) @@ -166,6 +188,26 @@ class ActivityRepositoryUnitTest { @Test fun documentToActivity() { + val activity1 = + Activity( + "activityUid", + "activityTitle", + "activityDescription", + Location(0.0, 0.0, Timestamp(0, 0), "location"), + Timestamp(0, 0), + listOf( + DocumentContainer( + mockDocumentDocumentReference, + mockTravelReference, + null, + "titleDoc", + DocumentFileFormat.JPEG, + 0L, + null, + null, + Timestamp(0, 0), + DocumentVisibility.ME))) + val privateFunc = activityRepositoryFirestore.javaClass.getDeclaredMethod( "documentToActivity", DocumentSnapshot::class.java) @@ -173,6 +215,6 @@ class ActivityRepositoryUnitTest { val parameters = arrayOfNulls(1) parameters[0] = mockDocumentSnapshot val result = privateFunc.invoke(activityRepositoryFirestore, *parameters) - assertThat(result, `is`(activity)) + assertThat(result, `is`(activity1)) } } diff --git a/app/src/test/java/com/github/se/travelpouch/model/activity/map/DirectionsViewModelTest.kt b/app/src/test/java/com/github/se/travelpouch/model/activity/map/DirectionsViewModelTest.kt index bcc5848d..6fb1be8b 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/activity/map/DirectionsViewModelTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/activity/map/DirectionsViewModelTest.kt @@ -36,14 +36,14 @@ class DirectionsViewModelTest { "description", Location(0.0, 0.0, Timestamp(0, 0), "location"), Timestamp(0, 0), - mapOf()), + emptyList()), Activity( "uid2", "title2", "description2", Location(0.0, 0.0, Timestamp(0, 0), "location2"), Timestamp(50, 0), - mapOf())) + emptyList())) @Before fun setUp() { diff --git a/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentRepositoryTest.kt b/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentRepositoryTest.kt index fcca686d..cacdea5b 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentRepositoryTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentRepositoryTest.kt @@ -3,6 +3,8 @@ package com.github.se.travelpouch.model.documents import android.util.Log import androidx.test.core.app.ApplicationProvider +import com.github.se.travelpouch.model.activity.Activity +import com.github.se.travelpouch.model.travels.Location import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.Task import com.google.firebase.FirebaseApp @@ -12,6 +14,7 @@ import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.QuerySnapshot +import com.google.firebase.firestore.Transaction import com.google.firebase.functions.FirebaseFunctions import com.google.firebase.storage.FirebaseStorage import junit.framework.TestCase.assertEquals @@ -29,6 +32,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @@ -137,13 +141,54 @@ class DocumentRepositoryTest { @Test fun deleteDocumentByIdSuccessfully() { + val mockFirebaseFirestoreBis: FirebaseFirestore = mock() + val mockDocumentRepository = + DocumentRepositoryFirestore(mockFirebaseFirestoreBis, mockStorage, mockAuth, mock()) + val mockCollectionReference: CollectionReference = mock() + + val privateField = mockDocumentRepository.javaClass.getDeclaredField("collectionPath") + privateField.isAccessible = true + privateField.set(mockDocumentRepository, "mockDocumentReferenceUser/documents") + + val transaction: Transaction = mock() + whenever(transaction.set(anyOrNull(), anyOrNull())).thenReturn(transaction) + whenever(transaction.delete(anyOrNull())).thenReturn(transaction) val task: Task = mock() - whenever(mockCollectionReference.document(anyString()).delete()).thenReturn(task) + + whenever(mockFirebaseFirestoreBis.runTransaction(anyOrNull())).thenReturn(task) + whenever(mockFirebaseFirestoreBis.collection(anyOrNull())).thenReturn(mockCollectionReference) + whenever(mockCollectionReference.document(anyOrNull())).thenReturn(mock()) whenever(task.isSuccessful).thenReturn(true) + whenever(task.addOnCompleteListener(anyOrNull())).thenReturn(task) + + val mockDocumentContainer: DocumentContainer = mock() + val mockDocumentContainerReference: DocumentReference = mock() + `when`(mockDocumentContainer.ref).thenReturn(mockDocumentContainerReference) + + val list = listOf(mockDocumentContainer) + + val activity = + Activity( + "qwertzuiopasdfghjkl1", + "title", + "description", + Location(0.0, 0.0, Timestamp.now(), "name"), + Timestamp.now(), + list) var successCalled = false - documentRepository.deleteDocumentById( - "documentId", { successCalled = true }, { fail("Should not call onFailure") }) + mockDocumentRepository.deleteDocumentById( + mockDocumentContainer, + listOf(activity), + { successCalled = true }, + { fail("Should not call onFailure") }) + + val transactionCaptor = argumentCaptor>() + verify(mockFirebaseFirestoreBis).runTransaction(transactionCaptor.capture()) + transactionCaptor.firstValue.apply(transaction) + + verify(transaction).delete(anyOrNull()) + verify(transaction, times(list.size)).set(anyOrNull(), anyOrNull()) val onCompleteListenerCaptor = argumentCaptor>() verify(task).addOnCompleteListener(onCompleteListenerCaptor.capture()) @@ -157,14 +202,14 @@ class DocumentRepositoryTest { val task: Task = mock() val exception = Exception("Firestore error") - whenever(mockCollectionReference.document(anyString()).delete()).thenReturn(task) + whenever(mockFirestore.runTransaction(anyOrNull())).thenReturn(task) whenever(task.isSuccessful).thenReturn(false) whenever(task.exception).thenReturn(exception) mockStatic(Log::class.java).use { logMock -> var failureCalled = false documentRepository.deleteDocumentById( - "documentId", { fail("Should not call onSuccess") }, { failureCalled = true }) + mock(), emptyList(), { fail("Should not call onSuccess") }, { failureCalled = true }) val onCompleteListenerCaptor = argumentCaptor>() verify(task).addOnCompleteListener(onCompleteListenerCaptor.capture()) diff --git a/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentViewModelTest.kt b/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentViewModelTest.kt index 0888a662..0e0176e0 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentViewModelTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/documents/DocumentViewModelTest.kt @@ -135,12 +135,12 @@ class DocumentViewModelTest { fun deleteDocumentById_successfulDelete_updatesDocuments() { val emptyDocumentList = emptyList() doAnswer { invocation -> - val onSuccess = invocation.getArgument(1) as () -> Unit + val onSuccess = invocation.getArgument(2) as () -> Unit onSuccess() null } .whenever(documentRepository) - .deleteDocumentById(anyOrNull(), anyOrNull(), anyOrNull()) + .deleteDocumentById(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) doAnswer { invocation -> val onSuccess = invocation.getArgument(0) as (List) -> Unit @@ -150,7 +150,7 @@ class DocumentViewModelTest { .whenever(documentRepository) .getDocuments(anyOrNull(), anyOrNull()) - documentViewModel.deleteDocumentById("1") + documentViewModel.deleteDocumentById(mock(DocumentContainer::class.java), emptyList()) assertThat(documentViewModel.documents.value, `is`(emptyDocumentList)) } @@ -201,19 +201,20 @@ class DocumentViewModelTest { val errorMessage = "Failed to delete Document" val exception = Exception("Delete Document Failed Test") doAnswer { invocation -> - val onFailure = invocation.getArgument(2) as (Exception) -> Unit + val onFailure = invocation.getArgument(3) as (Exception) -> Unit onFailure(exception) null } .whenever(documentRepository) - .deleteDocumentById(anyOrNull(), anyOrNull(), anyOrNull()) + .deleteDocumentById(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) mockStatic(Log::class.java).use { logMock: MockedStatic -> logMock.`when` { Log.e(anyString(), anyString(), any()) }.thenReturn(0) - documentViewModel.deleteDocumentById("1") + documentViewModel.deleteDocumentById(mock(DocumentContainer::class.java), emptyList()) - verify(documentRepository).deleteDocumentById(anyOrNull(), anyOrNull(), anyOrNull()) + verify(documentRepository) + .deleteDocumentById(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) logMock.verify { Log.e("DocumentsViewModel", errorMessage, exception) } } }