diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2361654e..0e4fd6a1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,8 @@ plugins { alias(libs.plugins.sonar) id("jacoco") alias(libs.plugins.gms) + id("kotlin-kapt") + id("com.google.dagger.hilt.android") } android { @@ -38,7 +40,7 @@ android { versionCode = 1 versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.github.se.travelpouch.HiltTestRunner" vectorDrawables { useSupportLibrary = true } @@ -99,12 +101,12 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } packaging { @@ -186,14 +188,16 @@ dependencies { globalTestImplementation(libs.androidx.junit) globalTestImplementation(libs.androidx.espresso.core) - //todo: wait for a listener to finish libraries - implementation(libs.guava) + //Hilt + implementation(libs.hilt.android) + kapt(libs.hilt.android.compiler) + kapt(libs.androidx.hilt.compiler) + implementation(libs.androidx.hilt.navigation.compose) - // To use CallbackToFutureAdapter - implementation(libs.androidx.concurrent.futures) - - // Kotlin - implementation(libs.kotlinx.coroutines.guava) + testImplementation(libs.hilt.android.testing) + kaptTest(libs.hilt.android.compiler) + androidTestImplementation(libs.hilt.android.testing) + kaptAndroidTest(libs.hilt.android.compiler) // Google Service and Maps @@ -276,6 +280,11 @@ dependencies { androidTestImplementation(libs.mockito.kotlin) } +// Allow references to generated code +kapt { + correctErrorTypes = true +} + tasks.withType { // Configure Jacoco for each tests configure { diff --git a/app/src/androidTest/java/com/github/se/travelpouch/HiltTestRunner.kt b/app/src/androidTest/java/com/github/se/travelpouch/HiltTestRunner.kt new file mode 100644 index 00000000..4ac11233 --- /dev/null +++ b/app/src/androidTest/java/com/github/se/travelpouch/HiltTestRunner.kt @@ -0,0 +1,16 @@ +package com.github.se.travelpouch + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +class HiltTestRunner : AndroidJUnitRunner() { + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} diff --git a/app/src/androidTest/java/com/github/se/travelpouch/di/TestModule.kt b/app/src/androidTest/java/com/github/se/travelpouch/di/TestModule.kt new file mode 100644 index 00000000..45c8fc1c --- /dev/null +++ b/app/src/androidTest/java/com/github/se/travelpouch/di/TestModule.kt @@ -0,0 +1,36 @@ +package com.github.se.travelpouch.di + +import com.github.se.travelpouch.model.authentication.AuthenticationService +import com.github.se.travelpouch.model.authentication.MockFirebaseAuthenticationService +import com.github.se.travelpouch.model.profile.ProfileRepository +import com.github.se.travelpouch.model.profile.ProfileRepositoryMock +import com.github.se.travelpouch.model.travels.TravelRepository +import com.github.se.travelpouch.model.travels.TravelRepositoryMock +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object TestModule { + + @Provides + @Singleton + fun provideFirebaseAuth(): AuthenticationService { + return MockFirebaseAuthenticationService() + } + + @Provides + @Singleton + fun providesProfileRepository(): ProfileRepository { + return ProfileRepositoryMock() + } + + @Provides + @Singleton + fun providesTravelRepository(): TravelRepository { + return TravelRepositoryMock() + } +} diff --git a/app/src/androidTest/java/com/github/se/travelpouch/endtoend/EndToEndTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/endtoend/EndToEndTest.kt new file mode 100644 index 00000000..450ed12c --- /dev/null +++ b/app/src/androidTest/java/com/github/se/travelpouch/endtoend/EndToEndTest.kt @@ -0,0 +1,123 @@ +package com.github.se.travelpouch.endtoend + +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import com.github.se.travelpouch.MainActivity +import com.github.se.travelpouch.di.AppModule +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@UninstallModules(AppModule::class) +class EndToEndTest { + + @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun verifyUserFlowForTravelCreation() = + runTest(timeout = 40.seconds) { + + // assert that login screen is displayed + composeTestRule.onNodeWithTag("appLogo").assertIsDisplayed() + composeTestRule.onNodeWithTag("welcomText").assertIsDisplayed() + composeTestRule.onNodeWithText("Sign in with email and password").assertIsDisplayed() + + // go to sign in screen with email and password and log in + composeTestRule.onNodeWithText("Sign in with email and password").performClick() + + composeTestRule.onNodeWithTag("emailField").assertIsDisplayed() + composeTestRule.onNodeWithTag("passwordField").assertIsDisplayed() + composeTestRule.onNodeWithText("Sign in").assertIsDisplayed() + + composeTestRule.onNodeWithTag("emailField").performTextInput("travelpouchtest1@gmail.com") + composeTestRule.onNodeWithTag("passwordField").performTextInput("travelpouchtest1password") + composeTestRule.onNodeWithText("Sign in").performClick() + + // wait until we are in the travel list screen + composeTestRule.waitUntil { + composeTestRule.onNodeWithTag("emptyTravelPrompt", useUnmergedTree = true).isDisplayed() + } + + // test that no travels are displayed because we have a new account + composeTestRule + .onNodeWithTag("emptyTravelPrompt", useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithTag("createTravelFab").assertIsDisplayed() + composeTestRule.onNodeWithTag("createTravelFab").performClick() + + // wait until we are in the screen to add a travel + composeTestRule.waitUntil { + composeTestRule.onNodeWithTag("travelTitle", useUnmergedTree = true).isDisplayed() + } + + // test that everything is displayed + composeTestRule.onNodeWithTag("travelTitle").assertIsDisplayed() + composeTestRule.onNodeWithTag("travelTitle").assertTextEquals("Create a new travel") + composeTestRule.onNodeWithTag("inputTravelTitle").assertIsDisplayed() + composeTestRule.onNodeWithTag("inputTravelDescription").assertIsDisplayed() + composeTestRule.onNodeWithTag("inputTravelLocation").assertIsDisplayed() + composeTestRule.onNodeWithTag("inputTravelStartDate").assertIsDisplayed() + composeTestRule.onNodeWithTag("inputTravelEndDate").assertIsDisplayed() + composeTestRule.onNodeWithTag("travelSaveButton").assertIsDisplayed() + composeTestRule.onNodeWithTag("travelSaveButton").assertTextEquals("Save") + + // input fields to create a travel + composeTestRule.onNodeWithTag("inputTravelTitle").performTextInput("e2e travel 1") + composeTestRule + .onNodeWithTag("inputTravelDescription") + .performTextInput("test travel description") + composeTestRule.onNodeWithTag("inputTravelLocation").performTextInput("L") + + // wait to have La paz displayed + composeTestRule.waitUntil { + composeTestRule.onNodeWithText("La Paz, Bolivia").isDisplayed() + } + + composeTestRule.onNodeWithText("La Paz, Bolivia").performClick() + composeTestRule.onNodeWithTag("inputTravelStartDate").performTextInput("10/11/2024") + composeTestRule.onNodeWithTag("inputTravelEndDate").performTextInput("20/11/2024") + + // save the travel and go back to the list of travels + composeTestRule.onNodeWithTag("travelSaveButton").performClick() + + // verify that the previous buttons are still here + composeTestRule + .onNodeWithTag("TravelListScreen", useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithTag("createTravelFab").assertIsDisplayed() + + // verify that the empty travel prompt does not exist since we saved a travel + composeTestRule + .onNodeWithTag("emptyTravelPrompt", useUnmergedTree = true) + .assertDoesNotExist() + + composeTestRule.onNodeWithText("e2e travel 1").assertIsDisplayed() + composeTestRule.onNodeWithText("e2e travel 1").assert(hasText("e2e travel 1")) + composeTestRule.onNodeWithText("e2e travel 1").assert(hasText("test travel description")) + composeTestRule.onNodeWithText("e2e travel 1").assert(hasText("La Paz, Bolivia")) + composeTestRule.onNodeWithText("e2e travel 1").assert(hasText("10/11/2024")) + } +} diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/authentication/SignInViewTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/authentication/SignInViewTest.kt index 1bb22bcd..0d768211 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/authentication/SignInViewTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/authentication/SignInViewTest.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals 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 com.github.se.travelpouch.model.profile.ProfileModelView import com.github.se.travelpouch.model.profile.ProfileRepository @@ -66,4 +67,13 @@ class SignInViewTest { composeTestRule.waitForIdle() composeTestRule.onNodeWithTag("loadingSpinner").assertIsDisplayed() } + + @Test + fun signInWithWmailAndPasswordIsDisplayed() { + composeTestRule.setContent { + SignInScreen(navigationActions = mockNavigationActions, profileModelView, travelViewModel) + } + + composeTestRule.onNodeWithText("Sign in with email and password").assertIsDisplayed() + } } diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/authentication/TestSignInEmailPasswordUI.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/authentication/TestSignInEmailPasswordUI.kt new file mode 100644 index 00000000..129af705 --- /dev/null +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/authentication/TestSignInEmailPasswordUI.kt @@ -0,0 +1,110 @@ +package com.github.se.travelpouch.ui.authentication + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +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.compose.ui.test.performTextInput +import com.github.se.travelpouch.model.authentication.AuthenticationService +import com.github.se.travelpouch.model.authentication.MockFirebaseAuthenticationService +import com.github.se.travelpouch.model.profile.ProfileModelView +import com.github.se.travelpouch.model.profile.ProfileRepository +import com.github.se.travelpouch.model.travels.ListTravelViewModel +import com.github.se.travelpouch.model.travels.TravelRepository +import com.github.se.travelpouch.ui.navigation.NavigationActions +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.verify + +class TestSignInEmailPasswordUI { + val mockNavigationActions = mock(NavigationActions::class.java) + val travelRepository = mock(TravelRepository::class.java) + val profileRepository = mock(ProfileRepository::class.java) + + val travelViewModel = ListTravelViewModel(travelRepository) + val profileModelView = ProfileModelView(profileRepository) + + val authenticationService: AuthenticationService = mock() + val authenticationServiceMock = MockFirebaseAuthenticationService() + + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun verifiesTopBarAppDisplayed() { + composeTestRule.setContent { + SignInWithPassword( + mockNavigationActions, profileModelView, travelViewModel, authenticationService) + } + + composeTestRule.onNodeWithTag("PasswordTitle").assertIsDisplayed() + composeTestRule.onNodeWithTag("PasswordTitle").assertTextEquals("Signing in with password") + + composeTestRule.onNodeWithTag("goBackButton").assertIsDisplayed() + composeTestRule.onNodeWithTag("goBackButton").performClick() + verify(mockNavigationActions).navigateTo(anyOrNull()) + } + + @Test + fun signingInWithPasswordCallsCreateUser() = + runTest(timeout = 20.seconds) { + composeTestRule.setContent { + SignInWithPassword( + mockNavigationActions, profileModelView, travelViewModel, authenticationService) + } + + composeTestRule.onNodeWithTag("emailField").assertIsDisplayed() + composeTestRule.onNodeWithTag("passwordField").assertIsDisplayed() + composeTestRule.onNodeWithText("Sign in").assertIsDisplayed() + + composeTestRule.onNodeWithTag("emailField").performTextInput("travelpouchtest1@gmail.com") + composeTestRule.onNodeWithTag("passwordField").performTextInput("travelpouchtest1password") + composeTestRule.onNodeWithText("Sign in").performClick() + + verify(authenticationService).createUser(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + + @Test + fun logInWithPasswordCallsLogin() = + runTest(timeout = 20.seconds) { + composeTestRule.setContent { + SignInWithPassword( + mockNavigationActions, profileModelView, travelViewModel, authenticationService) + } + + composeTestRule.onNodeWithTag("emailField").assertIsDisplayed() + composeTestRule.onNodeWithTag("passwordField").assertIsDisplayed() + composeTestRule.onNodeWithText("Log in").assertIsDisplayed() + + composeTestRule.onNodeWithTag("emailField").performTextInput("travelpouchtest1@gmail.com") + composeTestRule.onNodeWithTag("passwordField").performTextInput("travelpouchtest1password") + composeTestRule.onNodeWithText("Log in").performClick() + + verify(authenticationService).login(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + + @Test + fun logInWithPasswordCallsProfileVM() = + runTest(timeout = 20.seconds) { + composeTestRule.setContent { + SignInWithPassword( + mockNavigationActions, profileModelView, travelViewModel, authenticationServiceMock) + } + + composeTestRule.onNodeWithTag("emailField").assertIsDisplayed() + composeTestRule.onNodeWithTag("passwordField").assertIsDisplayed() + composeTestRule.onNodeWithText("Log in").assertIsDisplayed() + + composeTestRule.onNodeWithTag("emailField").performTextInput("travelpouchtest1@gmail.com") + composeTestRule.onNodeWithTag("passwordField").performTextInput("travelpouchtest1password") + composeTestRule.onNodeWithText("Log in").performClick() + + verify(profileRepository).initAfterLogin(anyOrNull()) + verify(mockNavigationActions).navigateTo(anyOrNull()) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4745919d..5d29eca0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> () + val documentViewModel: DocumentViewModel = + viewModel(factory = DocumentViewModel.Factory(context.contentResolver)) + val activityModelView: ActivityViewModel = viewModel(factory = ActivityViewModel.Factory) + val eventsViewModel: EventViewModel = viewModel(factory = EventViewModel.Factory) + // val profileModelView: ProfileModelView = viewModel(factory = ProfileModelView.Factory) + val profileModelView = hiltViewModel() - val calendarViewModel: CalendarViewModel = - viewModel(factory = CalendarViewModel.Factory(activityModelView)) + val calendarViewModel: CalendarViewModel = + viewModel(factory = CalendarViewModel.Factory(activityModelView)) - val notificationViewModel: NotificationViewModel = - viewModel(factory = NotificationViewModel.Factory) + val notificationViewModel: NotificationViewModel = + viewModel(factory = NotificationViewModel.Factory) - NavHost(navController = navController, startDestination = Route.DEFAULT) { - navigation( - startDestination = Screen.AUTH, - route = Route.DEFAULT, - ) { - composable(Screen.AUTH) { - SignInScreen(navigationActions, profileModelView, listTravelViewModel) - } + NavHost(navController = navController, startDestination = Route.DEFAULT) { + navigation( + startDestination = Screen.AUTH, + route = Route.DEFAULT, + ) { + composable(Screen.AUTH) { + SignInScreen(navigationActions, profileModelView, listTravelViewModel) + } - composable(Screen.TRAVEL_LIST) { - TravelListScreen( - navigationActions, - listTravelViewModel, - activityModelView, - eventsViewModel, - documentViewModel, - profileModelView) - } - composable(Screen.TRAVEL_ACTIVITIES) { - TravelActivitiesScreen(navigationActions, activityModelView) - } - composable(Screen.ADD_ACTIVITY) { AddActivityScreen(navigationActions, activityModelView) } - composable(Screen.EDIT_ACTIVITY) { EditActivity(navigationActions, activityModelView) } - composable(Screen.ADD_TRAVEL) { - AddTravelScreen(listTravelViewModel, navigationActions, profileModelView = profileModelView) - } - composable(Screen.EDIT_TRAVEL_SETTINGS) { - EditTravelSettingsScreen(listTravelViewModel, navigationActions) - } + composable(Screen.TRAVEL_LIST) { + TravelListScreen( + navigationActions, + listTravelViewModel, + activityModelView, + eventsViewModel, + documentViewModel, + profileModelView) + } + composable(Screen.TRAVEL_ACTIVITIES) { + TravelActivitiesScreen(navigationActions, activityModelView) + } + composable(Screen.ADD_ACTIVITY) { AddActivityScreen(navigationActions, activityModelView) } + composable(Screen.EDIT_ACTIVITY) { EditActivity(navigationActions, activityModelView) } + composable(Screen.ADD_TRAVEL) { + AddTravelScreen( + listTravelViewModel, navigationActions, profileModelView = profileModelView) + } + composable(Screen.EDIT_TRAVEL_SETTINGS) { + EditTravelSettingsScreen(listTravelViewModel, navigationActions) + } - composable(Screen.ACTIVITIES_MAP) { - ActivitiesMapScreen(activityModelView, navigationActions) - } + composable(Screen.ACTIVITIES_MAP) { + ActivitiesMapScreen(activityModelView, navigationActions) + } - composable(Screen.PARTICIPANT_LIST) { - ParticipantListScreen(listTravelViewModel, navigationActions) - } - composable(Screen.DOCUMENT_LIST) { - DocumentListScreen( - documentViewModel, - listTravelViewModel, - navigationActions, - onNavigateToDocumentPreview = { navigationActions.navigateTo(Screen.DOCUMENT_PREVIEW) }) - } - composable(Screen.DOCUMENT_PREVIEW) { DocumentPreview(documentViewModel, navigationActions) } - composable(Screen.TIMELINE) { TimelineScreen(eventsViewModel) } + composable(Screen.PARTICIPANT_LIST) { + ParticipantListScreen(listTravelViewModel, navigationActions) + } + composable(Screen.DOCUMENT_LIST) { + DocumentListScreen( + documentViewModel, + listTravelViewModel, + navigationActions, + onNavigateToDocumentPreview = { + navigationActions.navigateTo(Screen.DOCUMENT_PREVIEW) + }) + } + composable(Screen.DOCUMENT_PREVIEW) { + DocumentPreview(documentViewModel, navigationActions) + } + composable(Screen.TIMELINE) { TimelineScreen(eventsViewModel) } - composable(Screen.PROFILE) { ProfileScreen(navigationActions, profileModelView) } - composable(Screen.EDIT_PROFILE) { - ModifyingProfileScreen(navigationActions, profileModelView) - } + composable(Screen.PROFILE) { ProfileScreen(navigationActions, profileModelView) } + composable(Screen.EDIT_PROFILE) { + ModifyingProfileScreen(navigationActions, profileModelView) + } - composable(Screen.CALENDAR) { CalendarScreen(calendarViewModel, navigationActions) } + composable(Screen.CALENDAR) { CalendarScreen(calendarViewModel, navigationActions) } + + composable(Screen.SIGN_IN_PASSWORD) { + SignInWithPassword(navigationActions, profileModelView, listTravelViewModel, auth) + } + } } } } diff --git a/app/src/main/java/com/github/se/travelpouch/TravelPouchApp.kt b/app/src/main/java/com/github/se/travelpouch/TravelPouchApp.kt new file mode 100644 index 00000000..203acf48 --- /dev/null +++ b/app/src/main/java/com/github/se/travelpouch/TravelPouchApp.kt @@ -0,0 +1,6 @@ +package com.github.se.travelpouch + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp class TravelPouchApp : Application() diff --git a/app/src/main/java/com/github/se/travelpouch/di/AppModules.kt b/app/src/main/java/com/github/se/travelpouch/di/AppModules.kt new file mode 100644 index 00000000..037cd0cc --- /dev/null +++ b/app/src/main/java/com/github/se/travelpouch/di/AppModules.kt @@ -0,0 +1,46 @@ +package com.github.se.travelpouch.di + +import com.github.se.travelpouch.model.authentication.AuthenticationService +import com.github.se.travelpouch.model.authentication.FirebaseAuthenticationService +import com.github.se.travelpouch.model.profile.ProfileRepository +import com.github.se.travelpouch.model.profile.ProfileRepositoryFirebase +import com.github.se.travelpouch.model.travels.TravelRepository +import com.github.se.travelpouch.model.travels.TravelRepositoryFirestore +import com.google.firebase.Firebase +import com.google.firebase.auth.auth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.firestore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object AppModule { + + @Provides + @Singleton + fun provideFirebaseAuth(): AuthenticationService { + return FirebaseAuthenticationService(Firebase.auth) + } + + @Provides + @Singleton + fun provideFirebaseFirestore(): FirebaseFirestore { + return Firebase.firestore + } + + @Provides + @Singleton + fun providesProfileRepository(db: FirebaseFirestore): ProfileRepository { + return ProfileRepositoryFirebase(db) + } + + @Provides + @Singleton + fun providesTravelRepository(db: FirebaseFirestore): TravelRepository { + return TravelRepositoryFirestore(db) + } +} diff --git a/app/src/main/java/com/github/se/travelpouch/di/Database.kt b/app/src/main/java/com/github/se/travelpouch/di/Database.kt new file mode 100644 index 00000000..c60b2b4e --- /dev/null +++ b/app/src/main/java/com/github/se/travelpouch/di/Database.kt @@ -0,0 +1,7 @@ +package com.github.se.travelpouch.di + +import com.github.se.travelpouch.model.profile.Profile +import com.github.se.travelpouch.model.travels.TravelContainer + +var profileCollection = mutableMapOf() +var travelCollection = mutableMapOf() diff --git a/app/src/main/java/com/github/se/travelpouch/model/authentication/AuthenticationService.kt b/app/src/main/java/com/github/se/travelpouch/model/authentication/AuthenticationService.kt new file mode 100644 index 00000000..f82bfb2c --- /dev/null +++ b/app/src/main/java/com/github/se/travelpouch/model/authentication/AuthenticationService.kt @@ -0,0 +1,87 @@ +package com.github.se.travelpouch.model.authentication + +import android.content.ContentValues.TAG +import android.util.Log +import com.google.android.gms.tasks.Task +import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser + +interface AuthenticationService { + fun createUser( + email: String, + password: String, + onSuccess: (FirebaseUser?) -> Unit, + onFailure: (Task) -> Unit + ) + + fun login( + email: String, + password: String, + onSuccess: (FirebaseUser?) -> Unit, + onFailure: (Task) -> Unit + ) +} + +class FirebaseAuthenticationService(private val auth: FirebaseAuth) : AuthenticationService { + override fun createUser( + email: String, + password: String, + onSuccess: (FirebaseUser?) -> Unit, + onFailure: (Task) -> Unit + ) { + auth.createUserWithEmailAndPassword(email, password).addOnCompleteListener { task -> + if (task.isSuccessful) { + // Sign in success, update UI with the signed-in user's information + Log.d(TAG, "createUserWithEmail:success") + val user = auth.currentUser + + onSuccess(user) + } else { + // If sign in fails, display a message to the user. + onFailure(task) + } + } + } + + override fun login( + email: String, + password: String, + onSuccess: (FirebaseUser?) -> Unit, + onFailure: (Task) -> Unit + ) { + + auth.signInWithEmailAndPassword(email, password).addOnCompleteListener { task -> + if (task.isSuccessful) { + // Sign in success, update UI with the signed-in user's information + Log.d(TAG, "signInWithEmail:success") + val user = auth.currentUser + onSuccess(user) + } else { + // If sign in fails, display a message to the user. + Log.w(TAG, "signInWithEmail:failure", task.exception) + onFailure(task) + } + } + } +} + +class MockFirebaseAuthenticationService : AuthenticationService { + override fun createUser( + email: String, + password: String, + onSuccess: (FirebaseUser?) -> Unit, + onFailure: (Task) -> Unit + ) { + onSuccess(null) + } + + override fun login( + email: String, + password: String, + onSuccess: (FirebaseUser?) -> Unit, + onFailure: (Task) -> Unit + ) { + onSuccess(null) + } +} diff --git a/app/src/main/java/com/github/se/travelpouch/model/notifications/NotificationRepository.kt b/app/src/main/java/com/github/se/travelpouch/model/notifications/NotificationRepository.kt index eb32ba56..fb7f3667 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/notifications/NotificationRepository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/notifications/NotificationRepository.kt @@ -1,6 +1,7 @@ package com.github.se.travelpouch.model.notifications import android.util.Log +import com.github.se.travelpouch.model.FirebasePaths import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query @@ -12,10 +13,10 @@ import com.google.firebase.firestore.Query class NotificationRepository(private val firestore: FirebaseFirestore) { // Reference to the "notifications" collection in Firestore - private val notificationCollection = firestore.collection("notifications") + private val notificationCollection = FirebasePaths.notifications fun getNewUid(): String { - return notificationCollection.document().id + return firestore.collection(notificationCollection).document().id } /** @@ -24,7 +25,8 @@ class NotificationRepository(private val firestore: FirebaseFirestore) { * @param notification The notification to be added. */ fun addNotification(notification: Notification) { - notificationCollection + firestore + .collection(notificationCollection) .document(notification.notificationUid) .set(notification) .addOnSuccessListener { Log.d("NotificationRepository", "Notification added successfully") } @@ -44,7 +46,8 @@ class NotificationRepository(private val firestore: FirebaseFirestore) { userId: String, onNotificationFetched: (List) -> Unit ) { - notificationCollection + firestore + .collection(notificationCollection) .whereEqualTo("receiverId", userId) .orderBy("timestamp", Query.Direction.DESCENDING) .get() @@ -64,7 +67,10 @@ class NotificationRepository(private val firestore: FirebaseFirestore) { * @param notificationUid The UID of the notification to be marked as read. */ fun markNotificationAsRead(notificationUid: String) { - notificationCollection.document(notificationUid).update("status", NotificationStatus.READ) + firestore + .collection(notificationCollection) + .document(notificationUid) + .update("status", NotificationStatus.READ) } /** @@ -74,6 +80,9 @@ class NotificationRepository(private val firestore: FirebaseFirestore) { * @param notificationType The new type of the notification. */ fun changeNotificationType(notificationUid: String, notificationType: NotificationType) { - notificationCollection.document(notificationUid).update("notificationType", notificationType) + firestore + .collection(notificationCollection) + .document(notificationUid) + .update("notificationType", notificationType) } } diff --git a/app/src/main/java/com/github/se/travelpouch/model/profile/ProfileModelView.kt b/app/src/main/java/com/github/se/travelpouch/model/profile/ProfileModelView.kt index 2e9821d6..3789119f 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/profile/ProfileModelView.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/profile/ProfileModelView.kt @@ -4,9 +4,8 @@ import android.content.Context import android.util.Log import android.widget.Toast import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.google.firebase.Firebase -import com.google.firebase.firestore.firestore +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -18,19 +17,21 @@ import kotlinx.coroutines.flow.asStateFlow * @param repository (ProfileRepository) : the repository that is used as a logic between Firebase * and profiles */ -class ProfileModelView(private val repository: ProfileRepository) : ViewModel() { +@HiltViewModel +class ProfileModelView @Inject constructor(private val repository: ProfileRepository) : + ViewModel() { private val onFailureTag = "ProfileViewModel" - companion object { - val Factory: ViewModelProvider.Factory = - object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return ProfileModelView(ProfileRepositoryFirebase(Firebase.firestore)) as T - } - } - } + // companion object { + // val Factory: ViewModelProvider.Factory = + // object : ViewModelProvider.Factory { + // @Suppress("UNCHECKED_CAST") + // override fun create(modelClass: Class): T { + // return ProfileModelView(ProfileRepositoryFirebase(Firebase.firestore)) as T + // } + // } + // } private val profile_ = MutableStateFlow(ErrorProfile.errorProfile) val profile: StateFlow = profile_.asStateFlow() diff --git a/app/src/main/java/com/github/se/travelpouch/model/profile/ProfileRepositoryMock.kt b/app/src/main/java/com/github/se/travelpouch/model/profile/ProfileRepositoryMock.kt new file mode 100644 index 00000000..b93543ec --- /dev/null +++ b/app/src/main/java/com/github/se/travelpouch/model/profile/ProfileRepositoryMock.kt @@ -0,0 +1,41 @@ +package com.github.se.travelpouch.model.profile + +import com.github.se.travelpouch.di.profileCollection + +class ProfileRepositoryMock : ProfileRepository { + + var profilePath: String = "" + + override fun getProfileElements(onSuccess: (Profile) -> Unit, onFailure: (Exception) -> Unit) { + val profile: Profile? = profileCollection[profilePath] + if (profile == null) { + onFailure(Exception("error getting the profile")) + } else { + onSuccess(profile) + } + } + + override fun updateProfile( + newProfile: Profile, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit + ) { + profileCollection[profilePath] = newProfile + onSuccess() + } + + override suspend fun initAfterLogin(onSuccess: (Profile) -> Unit) { + + profilePath = "qwertzuiopasdfghjklyxcvbnm12" + val profileFetched: Profile? = profileCollection[profilePath] + + if (profileFetched != null) { + onSuccess(profileFetched) + } else { + val profile = + Profile(profilePath, "username", "emailtest1@gmail.com", null, "name", emptyList()) + profileCollection[profilePath] = profile + onSuccess(profile) + } + } +} diff --git a/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt index 2dfb909e..c9f2c36a 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt @@ -2,11 +2,10 @@ package com.github.se.travelpouch.model.travels import android.util.Log import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.github.se.travelpouch.model.profile.Profile -import com.google.firebase.Firebase -import com.google.firebase.firestore.firestore +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -17,7 +16,9 @@ import kotlinx.coroutines.launch * * @property repository The repository used for accessing travel data. */ -open class ListTravelViewModel(private val repository: TravelRepository) : ViewModel() { +@HiltViewModel +open class ListTravelViewModel @Inject constructor(private val repository: TravelRepository) : + ViewModel() { private val travels_ = MutableStateFlow>(emptyList()) val travels: StateFlow> = travels_.asStateFlow() @@ -37,16 +38,16 @@ open class ListTravelViewModel(private val repository: TravelRepository) : ViewM repository.initAfterLogin { getTravels() } } - // create factory - companion object { - val Factory: ViewModelProvider.Factory = - object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return ListTravelViewModel(TravelRepositoryFirestore(Firebase.firestore)) as T - } - } - } + // // create factory + // companion object { + // val Factory: ViewModelProvider.Factory = + // object : ViewModelProvider.Factory { + // @Suppress("UNCHECKED_CAST") + // override fun create(modelClass: Class): T { + // return ListTravelViewModel(TravelRepositoryFirestore(Firebase.firestore)) as T + // } + // } + // } /** * Generates a new unique ID. diff --git a/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryMock.kt b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryMock.kt new file mode 100644 index 00000000..d73df270 --- /dev/null +++ b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryMock.kt @@ -0,0 +1,66 @@ +package com.github.se.travelpouch.model.travels + +import com.github.se.travelpouch.di.travelCollection +import com.github.se.travelpouch.model.profile.Profile + +class TravelRepositoryMock : TravelRepository { + + private var currentUserUid = "" + + override fun getNewUid(): String { + return TravelContainerMock.generateAutoObjectId() + } + + override fun getParticipantFromfsUid( + fsUid: fsUid, + onSuccess: (Profile?) -> Unit, + onFailure: (Exception) -> Unit + ) { + TODO("Not yet implemented") + } + + override fun checkParticipantExists( + email: String, + onSuccess: (Profile?) -> Unit, + onFailure: (Exception) -> Unit + ) { + TODO("Not yet implemented") + } + + override fun initAfterLogin(onSuccess: () -> Unit) { + currentUserUid = "qwertzuiopasdfghjklyxcvbnm12" + onSuccess() + } + + override fun getTravels( + onSuccess: (List) -> Unit, + onFailure: (Exception) -> Unit + ) { + val allTravels = travelCollection.values.toList() + val travelsOfUsers = allTravels.filter { it.listParticipant.contains(currentUserUid) } + onSuccess(travelsOfUsers) + } + + override fun addTravel( + travel: TravelContainer, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit + ) { + travelCollection[travel.fsUid] = travel + onSuccess() + } + + override fun updateTravel( + travel: TravelContainer, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit + ) { + travelCollection[travel.fsUid] = travel + onSuccess() + } + + override fun deleteTravelById(id: String, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) { + com.github.se.travelpouch.di.travelCollection.remove(id) + onSuccess() + } +} diff --git a/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt b/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt index 3072a406..a57b79a7 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt @@ -15,14 +15,17 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -59,11 +62,13 @@ fun SignInScreen( ) { val context = LocalContext.current val isLoading: MutableState = isLoading + val methodChosen = rememberSaveable { mutableStateOf(false) } // launcher for Firebase authentication val launcher = rememberFirebaseAuthLauncher( onAuthComplete = { result -> + methodChosen.value = false Log.d("SignInScreen", "User signed in: ${result.user?.displayName}") val job = @@ -76,6 +81,7 @@ fun SignInScreen( navigationActions.navigateTo(Screen.TRAVEL_LIST) }, onAuthError = { + methodChosen.value = false isLoading.value = false Log.e("SignInScreen", "Failed to sign in: ${it.statusCode}") Toast.makeText(context, "Login Failed!", Toast.LENGTH_LONG).show() @@ -111,45 +117,54 @@ fun SignInScreen( Spacer(modifier = Modifier.height(48.dp)) - Box( - modifier = - Modifier.fillMaxWidth(0.8f) // Fixed width for both button and spinner - .height(56.dp), // Fixed height for both button and spinner - contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = + Modifier.fillMaxWidth(0.8f) // Fixed width for both button and spinner + .height(56.dp), // Fixed height for both button and spinner + contentAlignment = Alignment.Center) { - // Google Sign-In Button (before the loading state) - this@Column.AnimatedVisibility( - visible = !isLoading.value, - enter = fadeIn(animationSpec = tween(150)), - exit = fadeOut(animationSpec = tween(300))) { - // Assuming `GoogleSignInButton` is provided by Google Sign-In SDK - GoogleSignInButton( - onSignInClick = { - val gso = - GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(token) - .requestEmail() - .build() - val googleSignInClient = GoogleSignIn.getClient(context, gso) - launcher.launch(googleSignInClient.signInIntent) - isLoading.value = true - }) - } + // Google Sign-In Button (before the loading state) + this@Column.AnimatedVisibility( + visible = !isLoading.value, + enter = fadeIn(animationSpec = tween(150)), + exit = fadeOut(animationSpec = tween(300))) { + // Assuming `GoogleSignInButton` is provided by Google Sign-In SDK + GoogleSignInButton( + onSignInClick = { + methodChosen.value = true + val gso = + GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(token) + .requestEmail() + .build() + val googleSignInClient = GoogleSignIn.getClient(context, gso) + launcher.launch(googleSignInClient.signInIntent) + isLoading.value = true + }) + } - // CircularProgressIndicator (when loading) - this@Column.AnimatedVisibility( - visible = isLoading.value, - enter = fadeIn(animationSpec = tween(300)), - exit = fadeOut(animationSpec = tween(300))) { - CircularProgressIndicator( - modifier = - Modifier.height(28.dp) - .testTag( - "loadingSpinner"), // Same height as Google Sign-In button - color = MaterialTheme.colorScheme.primary, - strokeWidth = 5.dp) - } - } + // CircularProgressIndicator (when loading) + this@Column.AnimatedVisibility( + visible = isLoading.value, + enter = fadeIn(animationSpec = tween(300)), + exit = fadeOut(animationSpec = tween(300))) { + CircularProgressIndicator( + modifier = + Modifier.height(28.dp) + .testTag( + "loadingSpinner"), // Same height as Google Sign-In button + color = MaterialTheme.colorScheme.primary, + strokeWidth = 5.dp) + } + } + + Button( + onClick = { navigationActions.navigateTo(Screen.SIGN_IN_PASSWORD) }, + enabled = !methodChosen.value) { + Text("Sign in with email and password") + } + } } }) } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignInWithPassword.kt b/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignInWithPassword.kt new file mode 100644 index 00000000..224b47c3 --- /dev/null +++ b/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignInWithPassword.kt @@ -0,0 +1,171 @@ +package com.github.se.travelpouch.ui.authentication + +import android.annotation.SuppressLint +import android.content.ContentValues.TAG +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.github.se.travelpouch.model.authentication.AuthenticationService +import com.github.se.travelpouch.model.profile.ProfileModelView +import com.github.se.travelpouch.model.profile.isValidEmail +import com.github.se.travelpouch.model.travels.ListTravelViewModel +import com.github.se.travelpouch.ui.navigation.NavigationActions +import com.github.se.travelpouch.ui.navigation.Screen +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("CoroutineCreationDuringComposition") +@Composable +fun SignInWithPassword( + navigationActions: NavigationActions, + profileModelView: ProfileModelView, + travelViewModel: ListTravelViewModel, + authService: AuthenticationService +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + val methodChosen = rememberSaveable { mutableStateOf(false) } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { Text("Signing in with password", Modifier.testTag("PasswordTitle")) }, + navigationIcon = { + IconButton( + onClick = { navigationActions.navigateTo(Screen.AUTH) }, + modifier = Modifier.testTag("goBackButton")) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back") + } + }) + }) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + OutlinedTextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.testTag("emailField"), + label = { Text("Email") }, + placeholder = { Text("example@example.com") }) + + Spacer(Modifier.padding(10.dp)) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier.testTag("passwordField"), + label = { Text("Password") }, + placeholder = { Text("password") }, + visualTransformation = PasswordVisualTransformation()) + + Spacer(Modifier.padding(10.dp)) + + Button( + enabled = + email.isNotBlank() && + password.isNotBlank() && + isValidEmail(email) && + !methodChosen.value, + onClick = { + methodChosen.value = true + authService.createUser( + email, + password, + onSuccess = { user -> + Log.d("SignInScreen", "User signed in: ${user?.displayName}") + + coroutineScope.launch { + profileModelView.initAfterLogin { travelViewModel.initAfterLogin() } + } + + Toast.makeText(context, "Login successful", Toast.LENGTH_LONG).show() + navigationActions.navigateTo(Screen.TRAVEL_LIST) + }, + onFailure = { task -> + methodChosen.value = false + Log.w(TAG, "createUserWithEmail:failure", task.exception) + Toast.makeText( + context, + "Signing in failed.", + Toast.LENGTH_SHORT, + ) + .show() + }) + }) { + Text("Sign in") + } + + Spacer(Modifier.padding(5.dp)) + + Button( + enabled = + email.isNotBlank() && + password.isNotBlank() && + isValidEmail(email) && + !methodChosen.value, + onClick = { + methodChosen.value = true + authService.login( + email, + password, + onSuccess = { user -> + Log.d("SignInScreen", "User logged in: ${user?.displayName}") + + coroutineScope.launch { + profileModelView.initAfterLogin { travelViewModel.initAfterLogin() } + } + + Toast.makeText(context, "Login successful", Toast.LENGTH_LONG).show() + navigationActions.navigateTo(Screen.TRAVEL_LIST) + }, + onFailure = { task -> + Log.w(TAG, "LoginWithEmailAndPassword:failure", task.exception) + Toast.makeText( + context, + "Authentication failed.", + Toast.LENGTH_SHORT, + ) + .show() + methodChosen.value = false + }) + }) { + Text("Log in") + } + } + } +} diff --git a/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentList.kt b/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentList.kt index c7dde36d..b2e4aa01 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentList.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentList.kt @@ -75,7 +75,7 @@ import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult @Composable fun DocumentListScreen( documentViewModel: DocumentViewModel = viewModel(), - listTravelViewModel: ListTravelViewModel = viewModel(factory = ListTravelViewModel.Factory), + listTravelViewModel: ListTravelViewModel, navigationActions: NavigationActions, onNavigateToDocumentPreview: () -> Unit ) { diff --git a/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt b/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt index 02f7a727..cdf9513c 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt @@ -64,7 +64,7 @@ import com.google.firebase.Timestamp @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddTravelScreen( - listTravelViewModel: ListTravelViewModel = viewModel(factory = ListTravelViewModel.Factory), + listTravelViewModel: ListTravelViewModel, navigationActions: NavigationActions, locationViewModel: LocationViewModel = viewModel(factory = LocationViewModel.Factory), profileModelView: ProfileModelView diff --git a/app/src/main/java/com/github/se/travelpouch/ui/navigation/NavigationActions.kt b/app/src/main/java/com/github/se/travelpouch/ui/navigation/NavigationActions.kt index 4506cc66..a8608107 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/navigation/NavigationActions.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/navigation/NavigationActions.kt @@ -30,6 +30,7 @@ object Screen { const val ACTIVITIES_MAP = "MapActivities Screen" const val CALENDAR = "Calendar Screen" + const val SIGN_IN_PASSWORD = "Sign in with password Screen" } data class TopLevelDestination(val screen: String, val icon: ImageVector, val textId: String) diff --git a/app/src/test/java/com/github/se/travelpouch/model/authentication/TestSignInWithEmailAndPassword.kt b/app/src/test/java/com/github/se/travelpouch/model/authentication/TestSignInWithEmailAndPassword.kt new file mode 100644 index 00000000..396d23d4 --- /dev/null +++ b/app/src/test/java/com/github/se/travelpouch/model/authentication/TestSignInWithEmailAndPassword.kt @@ -0,0 +1,126 @@ +package com.github.se.travelpouch.model.authentication + +import com.google.android.gms.tasks.OnCompleteListener +import com.google.android.gms.tasks.Task +import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import junit.framework.TestCase.assertFalse +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class TestSignInWithEmailAndPassword { + private lateinit var mockFirebaseAuth: FirebaseAuth + + @Before + fun setUp() { + mockFirebaseAuth = mock(FirebaseAuth::class.java) + } + + @Test + fun testSignInWithEmailAndPasswordWorksCorrectly() = + runTest(timeout = 30.seconds) { + val firebaseAuthService = FirebaseAuthenticationService(mockFirebaseAuth) + val task: Task = mock() + val mockUser: FirebaseUser = mock() + + whenever(task.isSuccessful).thenReturn(true) + whenever(mockFirebaseAuth.createUserWithEmailAndPassword(anyOrNull(), anyOrNull())) + .thenReturn(task) + whenever(mockFirebaseAuth.currentUser).thenReturn(mockUser) + + var onSuccessCalled = false + var onFailureCalled = false + + firebaseAuthService.createUser( + "email", "password", { onSuccessCalled = true }, { onFailureCalled = true }) + + val onCompleteListenerCaptor = argumentCaptor>() + verify(task).addOnCompleteListener(onCompleteListenerCaptor.capture()) + onCompleteListenerCaptor.firstValue.onComplete(task) + + assert(onSuccessCalled) + assertFalse(onFailureCalled) + } + + @Test + fun testSignInWithEmailAndPasswordIfTaskFails() = + runTest(timeout = 30.seconds) { + val firebaseAuthService = FirebaseAuthenticationService(mockFirebaseAuth) + val task: Task = mock() + + var onSuccessCalled = false + var onFailureCalled = false + + whenever(task.isSuccessful).thenReturn(false) + whenever(mockFirebaseAuth.createUserWithEmailAndPassword(anyOrNull(), anyOrNull())) + .thenReturn(task) + + firebaseAuthService.createUser( + "email", "password", { onSuccessCalled = true }, { onFailureCalled = true }) + + val onCompleteListenerCaptor = argumentCaptor>() + verify(task).addOnCompleteListener(onCompleteListenerCaptor.capture()) + onCompleteListenerCaptor.firstValue.onComplete(task) + + assertFalse(onSuccessCalled) + assert(onFailureCalled) + } + + @Test + fun testLogInWithEmailAndPasswordWorksCorrectly() = + runTest(timeout = 30.seconds) { + val firebaseAuthService = FirebaseAuthenticationService(mockFirebaseAuth) + val task: Task = mock() + val mockUser: FirebaseUser = mock() + + whenever(task.isSuccessful).thenReturn(true) + whenever(mockFirebaseAuth.signInWithEmailAndPassword(anyOrNull(), anyOrNull())) + .thenReturn(task) + whenever(mockFirebaseAuth.currentUser).thenReturn(mockUser) + + var onSuccessCalled = false + var onFailureCalled = false + + firebaseAuthService.login( + "email", "password", { onSuccessCalled = true }, { onFailureCalled = true }) + + val onCompleteListenerCaptor = argumentCaptor>() + verify(task).addOnCompleteListener(onCompleteListenerCaptor.capture()) + onCompleteListenerCaptor.firstValue.onComplete(task) + + assert(onSuccessCalled) + assertFalse(onFailureCalled) + } + + @Test + fun testLogInWithEmailAndPasswordIfTaskFails() = + runTest(timeout = 30.seconds) { + val firebaseAuthService = FirebaseAuthenticationService(mockFirebaseAuth) + val task: Task = mock() + + var onSuccessCalled = false + var onFailureCalled = false + + whenever(task.isSuccessful).thenReturn(false) + whenever(mockFirebaseAuth.signInWithEmailAndPassword(anyOrNull(), anyOrNull())) + .thenReturn(task) + + firebaseAuthService.login( + "email", "password", { onSuccessCalled = true }, { onFailureCalled = true }) + + val onCompleteListenerCaptor = argumentCaptor>() + verify(task).addOnCompleteListener(onCompleteListenerCaptor.capture()) + onCompleteListenerCaptor.firstValue.onComplete(task) + + assertFalse(onSuccessCalled) + assert(onFailureCalled) + } +} diff --git a/app/src/test/java/com/github/se/travelpouch/model/notifications/NotificationRepositoryUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/notifications/NotificationRepositoryUnitTest.kt index c01633a2..155bff11 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/notifications/NotificationRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/notifications/NotificationRepositoryUnitTest.kt @@ -149,7 +149,7 @@ class NotificationRepositoryUnitTest { "6NU2zp2oGdA34s1Q1q5h12345678", "6NU2zp2oGdA34s1Q122212345678", "6NU2zp2oGdA34s1Q1q5h", - mock(NotificationContent::class.java), + mock(NotificationContent.InvitationNotification::class.java), NotificationType.INVITATION) whenever(querySnapshot.documents).thenReturn(listOf(queryDocumentSnapshot)) whenever(queryDocumentSnapshot.toObject(Notification::class.java)).thenReturn(mockNotification) diff --git a/app/src/test/java/com/github/se/travelpouch/model/profile/ProfileRepositoryMockTest.kt b/app/src/test/java/com/github/se/travelpouch/model/profile/ProfileRepositoryMockTest.kt new file mode 100644 index 00000000..d4f504b6 --- /dev/null +++ b/app/src/test/java/com/github/se/travelpouch/model/profile/ProfileRepositoryMockTest.kt @@ -0,0 +1,72 @@ +package com.github.se.travelpouch.model.profile + +import com.github.se.travelpouch.di.profileCollection +import junit.framework.TestCase.assertFalse +import org.junit.Before +import org.junit.Test + +class ProfileRepositoryMockTest { + + val profile = + Profile( + "qwertzuiopasdfghjklyxcvbnm12", + "username", + "emailtest1@gmail.com", + null, + "name", + emptyList()) + + val newProfile = + Profile( + "qwertzuiopasdfghjklyxcvbnm12", + "username - modified", + "emailtest1@gmail.com", + null, + "name", + emptyList()) + + val profileMockRepository = ProfileRepositoryMock() + + @Before + fun setup() { + profileMockRepository.profilePath = profile.fsUid + } + + @Test + fun verifiesThatGettingWorks() { + var succeeded = false + var failed = false + + val noProfile = com.github.se.travelpouch.di.profileCollection[profile.fsUid] + assert(noProfile == null) + profileMockRepository.getProfileElements({ succeeded = true }, { failed = true }) + assertFalse(succeeded) + assert(failed) + + succeeded = false + failed = false + + com.github.se.travelpouch.di.profileCollection[profile.fsUid] = profile + profileMockRepository.getProfileElements({ succeeded = true }, { failed = true }) + + assert(succeeded) + assertFalse(failed) + + com.github.se.travelpouch.di.profileCollection.clear() + } + + @Test + fun verifiesThatUpdatingWorks() { + var succeeded = false + var failed = false + + com.github.se.travelpouch.di.profileCollection[profile.fsUid] = profile + profileMockRepository.updateProfile(newProfile, { succeeded = true }, { failed = true }) + + assert(succeeded) + assertFalse(failed) + assert(com.github.se.travelpouch.di.profileCollection[profile.fsUid] == newProfile) + + com.github.se.travelpouch.di.profileCollection.clear() + } +} diff --git a/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryMockTest.kt b/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryMockTest.kt new file mode 100644 index 00000000..ff7c95b6 --- /dev/null +++ b/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryMockTest.kt @@ -0,0 +1,98 @@ +package com.github.se.travelpouch.model.travel + +import com.github.se.travelpouch.di.travelCollection +import com.github.se.travelpouch.model.travels.Location +import com.github.se.travelpouch.model.travels.Participant +import com.github.se.travelpouch.model.travels.Role +import com.github.se.travelpouch.model.travels.TravelContainer +import com.github.se.travelpouch.model.travels.TravelContainerMock +import com.github.se.travelpouch.model.travels.TravelRepositoryMock +import com.google.firebase.Timestamp +import junit.framework.TestCase.assertFalse +import org.junit.Test + +class TravelRepositoryMockTest { + + val travel = + TravelContainer( + "qwertzuiopasdfghjkly", + "title", + "description", + Timestamp(0, 0), + Timestamp.now(), + Location(0.0, 0.0, Timestamp(0, 0), "name"), + emptyMap(), + mapOf(Participant(TravelContainerMock.generateAutoUserId()) to Role.OWNER), + emptyList()) + + val travelUpdated = + TravelContainer( + "qwertzuiopasdfghjkly", + "title - modified", + "description", + Timestamp(0, 0), + Timestamp.now(), + Location(0.0, 0.0, Timestamp(0, 0), "name"), + emptyMap(), + mapOf(Participant(TravelContainerMock.generateAutoUserId()) to Role.OWNER), + emptyList()) + + val travelMockRepository = TravelRepositoryMock() + + @Test + fun verifiesThatAddingWorks() { + var succeeded = false + var failed = false + + val noTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(noTravel == null) + travelMockRepository.addTravel(travel, { succeeded = true }, { failed = true }) + assert(succeeded) + assertFalse(failed) + + val newTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(newTravel == travel) + + com.github.se.travelpouch.di.travelCollection.clear() + } + + @Test + fun verifiesThatUpdatingWorks() { + var succeeded = false + var failed = false + + val noTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(noTravel == null) + travelMockRepository.addTravel(travel, {}, {}) + val travelAdded = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(travelAdded == travel) + travelMockRepository.updateTravel(travelUpdated, { succeeded = true }, { failed = true }) + assert(succeeded) + assertFalse(failed) + + val newTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(newTravel == travelUpdated) + + com.github.se.travelpouch.di.travelCollection.clear() + } + + @Test + fun verifiesThatDeletingWorks() { + var succeeded = false + var failed = false + + val noTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(noTravel == null) + travelMockRepository.addTravel(travel, {}, {}) + val travelAdded = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(travelAdded == travel) + travelMockRepository.deleteTravelById(travel.fsUid, { succeeded = true }, { failed = true }) + assert(succeeded) + assertFalse(failed) + + val newTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] + assert(newTravel == null) + + com.github.se.travelpouch.di.travelCollection.clear() + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 7785a653..e857a063 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.androidApplication) apply false alias(libs.plugins.jetbrainsKotlinAndroid) apply false alias(libs.plugins.gms) apply false + id("com.google.dagger.hilt.android") version "2.51.1" apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d1243e0..2876f2ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,11 @@ agp = "8.3.2" concurrentFutures = "1.2.0" coreTesting = "2.2.0" guava = "33.0.0-android" +hiltAndroidCompiler = "2.51.1" +hiltCompiler = "1.2.0" +hiltLifecycleViewmodel = "1.0.0-alpha03" +hiltNavigationCompose = "1.0.0" +hiltNavigationComposeVersion = "1.2.0" kotlin = "1.8.10" coreKtx = "1.13.1" kotlinxCoroutinesGuava = "1.6.0" @@ -69,8 +74,15 @@ bouquet = "1.1.2" androidx-concurrent-futures = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrentFutures" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" } +androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" } +androidx-hilt-lifecycle-viewmodel = { module = "androidx.hilt:hilt-lifecycle-viewmodel", version.ref = "hiltLifecycleViewmodel" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationComposeVersion" } androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "uiTestJunit4" } guava = { module = "com.google.guava:guava", version.ref = "guava" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidCompiler" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hiltAndroidCompiler" } +hilt-navigation-compose = { module = "androix.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }