From 18c7138b133a0067b9cd19d1d91b745e9dcc4cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20S?= Date: Fri, 10 Jun 2022 02:21:10 +0200 Subject: [PATCH 1/3] yay --- .../java/ch/epfl/sweng/rps/MatchmakingTest.kt | 141 ++++++++++++++++-- .../java/ch/epfl/sweng/rps/TestUtils.kt | 45 ++++++ .../java/ch/epfl/sweng/rps/MainActivity.kt | 2 - .../sweng/rps/services/FirebaseGameService.kt | 2 +- .../sweng/rps/services/TestServiceLocator.kt | 12 +- .../sweng/rps/ui/game/MatchmakingFragment.kt | 38 +++-- .../java/ch/epfl/sweng/rps/utils/Utils.kt | 5 + app/src/main/res/values/strings.xml | 10 +- 8 files changed, 217 insertions(+), 38 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/MatchmakingTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/MatchmakingTest.kt index 27211df3..0915e326 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/MatchmakingTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/MatchmakingTest.kt @@ -1,35 +1,152 @@ package ch.epfl.sweng.rps +import android.content.Intent import android.view.View import android.widget.EditText import android.widget.TextView +import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.platform.app.InstrumentationRegistry +import ch.epfl.sweng.rps.TestUtils.waitForView +import ch.epfl.sweng.rps.models.remote.Game +import ch.epfl.sweng.rps.models.remote.GameMode +import ch.epfl.sweng.rps.models.remote.User +import ch.epfl.sweng.rps.persistence.Cache +import ch.epfl.sweng.rps.remote.Env +import ch.epfl.sweng.rps.services.FirebaseGameService +import ch.epfl.sweng.rps.services.MatchmakingService +import ch.epfl.sweng.rps.services.ServiceLocator +import ch.epfl.sweng.rps.services.TestServiceLocator +import ch.epfl.sweng.rps.ui.game.MatchmakingFragment +import ch.epfl.sweng.rps.utils.TEST_MODE +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.TypeSafeMatcher -import org.junit.Rule import org.junit.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class MatchmakingTest { - @get:Rule - val rule = ActivityScenarioRuleWithSetup.default(MainActivity::class.java) + data class Cfg(val currentGameId: String?, val gameMode: GameMode) + + private fun firebaseGameService(gameId: String, gameMode: GameMode): FirebaseGameService { + val m = mockk() + every { m.gameId } returns gameId + every { m.opponentCount() } returns flow { + for (i in 0 until gameMode.playerCount) { + emit(FirebaseGameService.PlayerCount.Some(i)) + delay(50) + } + emit(FirebaseGameService.PlayerCount.Full(gameMode.playerCount)) + + } + val game = mockk() + every { game.game_mode } returns gameMode.toGameModeString() + every { game.gameMode } returns gameMode + every { game.id } returns gameId + every { m.currentGame } returns game + coEvery { m.waitForGameStart() } answers { + runBlocking { + delay(50) + true + } + } + coEvery { m.refreshGame() } answers { + runBlocking { + delay(50) + mockk() + } + } + return m + } + + @Test + fun testMatchmakingWithExistingGame() { + setup(Cfg("gameId", GameMode.default(2))) + val scenario = ActivityScenario.launch(Intent( + InstrumentationRegistry.getInstrumentation().targetContext, + MainActivity::class.java, + ).apply { + putExtra(TEST_MODE, true) + }) + + scenario.use { + onView(withId(R.id.button_play_1_games_online)).perform(click()) + onView(withId(R.id.matchmaking_fragment)).check(matches(isDisplayed())) + onView(isRoot()).perform(waitForView(withText(R.string.ready_to_play), 10_000)) + } + } @Test - fun testMatchmaking() { - onView(withId(R.id.button_play_1_games_online)).perform(click()) - onView(withId(R.id.matchmaking_fragment)).check(matches(isDisplayed())) - onView(withId(R.id.matchmaking_status_textview)).check(matches(textDoes { - it.lowercase().contains("error") - })) + fun testMatchmakingWithoutExistingGame() { + setup(Cfg(null, GameMode.default(2))) + val scenario = ActivityScenario.launch(Intent( + InstrumentationRegistry.getInstrumentation().targetContext, + MainActivity::class.java, + ).apply { + putExtra(TEST_MODE, true) + }) + + scenario.use { + onView(withId(R.id.button_play_1_games_online)).perform(click()) + onView(withId(R.id.matchmaking_fragment)).check(matches(isDisplayed())) + onView(isRoot()).perform(waitForView(withText(R.string.ready_to_play), 10_000)) + } + } + + private fun setup(cfg: Cfg) { + ServiceLocator.setCurrentEnv(Env.Test) + Cache.Companion.initialize(InstrumentationRegistry.getInstrumentation().targetContext) + ServiceLocator.localRepository.setCurrentUid("user1") + ServiceLocator.localRepository.users["user1"] = User( + "user 1", "user1", User.Privacy.PUBLIC.name, email = "user1@example.com" + ) + val mm = mockk() + coEvery { mm.currentGame() } answers { + if (cfg.currentGameId != null) firebaseGameService(cfg.currentGameId, cfg.gameMode) + else null + } + every { mm.queue(any()) } returns flow { + delay(50) + emit(MatchmakingService.QueueStatus.Queued(cfg.gameMode)) + delay(50) + emit(MatchmakingService.QueueStatus.GameJoined(firebaseGameService("gameId", cfg.gameMode))) + } + val sl = ServiceLocator.getInstance() as TestServiceLocator + sl.matchmakingService = mm + } + + @Test + fun testMatchmakingException(): Unit = runBlocking { + val matchmakingTimeoutException = MatchmakingFragment.MatchmakingTimeoutException("test", 1000L) + val message = matchmakingTimeoutException.message + assertTrue(message.contains("test")) + + MatchmakingFragment.exceptionToString(Exception("test")) + MatchmakingFragment.exceptionToString(matchmakingTimeoutException) + + val timeoutCancellationException = kotlin.runCatching { + withTimeout(0) { + } + }.exceptionOrNull() as? TimeoutCancellationException + assertNotNull(timeoutCancellationException) + MatchmakingFragment.exceptionToString(timeoutCancellationException) } - fun textDoes(predicate: (String) -> Boolean): Matcher? { + fun textDoes(predicate: (String) -> Boolean): Matcher { return object : TypeSafeMatcher() { override fun describeTo(description: Description) { description.appendText("Has EditText/TextView the value: $predicate") diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/TestUtils.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/TestUtils.kt index 5f67e8a5..3155ddd8 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/TestUtils.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/TestUtils.kt @@ -6,9 +6,13 @@ import android.os.Bundle import android.util.Log import android.view.View import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.PerformException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.util.HumanReadables +import androidx.test.espresso.util.TreeIterables import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry import androidx.test.runner.lifecycle.Stage @@ -54,6 +58,47 @@ object TestUtils { } } + /** + * This ViewAction tells espresso to wait till a certain view is found in the view hierarchy. + * @param viewId The id of the view to wait for. + * @param timeout The maximum time which espresso will wait for the view to show up (in milliseconds) + */ + fun waitForView(matcher: Matcher, timeout: Long = 10_000): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher { + return isRoot() + } + + override fun getDescription(): String { + return "wait for a specific view during $timeout millis." + } + + override fun perform(uiController: UiController, rootView: View) { + uiController.loopMainThreadUntilIdle() + val startTime = System.currentTimeMillis() + val endTime = startTime + timeout + + do { + // Iterate through all views on the screen and see if the view we are looking for is there already + for (child in TreeIterables.breadthFirstViewTraversal(rootView)) { + // found view with required ID + if (matcher.matches(child)) { + return + } + } + // Loops the main thread for a specified period of time. + // Control may not return immediately, instead it'll return after the provided delay has passed and the queue is in an idle state again. + uiController.loopMainThreadForAtLeast(100) + } while (System.currentTimeMillis() < endTime) // in case of a timeout we throw an exception -> test fails + throw PerformException.Builder() + .withCause(TimeoutException()) + .withActionDescription(this.description) + .withViewDescription(HumanReadables.describe(rootView)) + .build() + } + } + } + fun retry( maxRetries: Int = 3, retryDelay: Long = 1000L, diff --git a/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt b/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt index a1991b36..e68382c0 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt @@ -1,7 +1,6 @@ package ch.epfl.sweng.rps import android.os.Bundle -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.navigation.findNavController import androidx.navigation.ui.setupWithNavController @@ -22,5 +21,4 @@ class MainActivity : AppCompatActivity() { navView.setupWithNavController(navController) } - } diff --git a/app/src/main/java/ch/epfl/sweng/rps/services/FirebaseGameService.kt b/app/src/main/java/ch/epfl/sweng/rps/services/FirebaseGameService.kt index af2b549d..2a07c00a 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/services/FirebaseGameService.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/services/FirebaseGameService.kt @@ -156,7 +156,7 @@ class FirebaseGameService( super.dispose() } - suspend fun opponentCount(): Flow = callbackFlow { + fun opponentCount(): Flow = callbackFlow { if (isGameFull) { send(PlayerCount.Full(currentGame.players.size)) channel.close() diff --git a/app/src/main/java/ch/epfl/sweng/rps/services/TestServiceLocator.kt b/app/src/main/java/ch/epfl/sweng/rps/services/TestServiceLocator.kt index b1992cb6..9ea26a7d 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/services/TestServiceLocator.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/services/TestServiceLocator.kt @@ -11,12 +11,12 @@ internal class TestServiceLocator : ServiceLocator { override val env: Env = Env.Test override val cachedGameServices: List get() = listOf() - override val matchmakingService: MatchmakingService - get() = object : MatchmakingService() { - override fun queue(gameMode: GameMode): Flow = flow { } - override suspend fun currentGame(): FirebaseGameService = - throw IllegalArgumentException() - } + + override var matchmakingService: MatchmakingService = object : MatchmakingService() { + override fun queue(gameMode: GameMode): Flow = flow { } + override suspend fun currentGame(): FirebaseGameService = + throw IllegalArgumentException() + } override fun dispose() { } diff --git a/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt b/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt index af34db4c..c7c220a9 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt @@ -1,7 +1,6 @@ package ch.epfl.sweng.rps.ui.game import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -18,6 +17,7 @@ import ch.epfl.sweng.rps.services.MatchmakingService import ch.epfl.sweng.rps.services.MatchmakingService.QueueStatus import ch.epfl.sweng.rps.services.ServiceLocator import ch.epfl.sweng.rps.utils.L +import ch.epfl.sweng.rps.utils.TEST_MODE import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first @@ -63,6 +63,11 @@ class MatchmakingFragment : Fragment() { if (loading) View.VISIBLE else View.GONE } + private fun isTest(): Boolean { + requireActivity() + return requireActivity().intent.getBooleanExtra(TEST_MODE, false) + } + private suspend fun startMatchmaking() { val mm = ServiceLocator.getInstance().matchmakingService var rounds = arguments?.getInt("rounds") @@ -71,6 +76,7 @@ class MatchmakingFragment : Fragment() { write("Error: rounds is null") return } + rounds < 1 -> { write( "WARNING: rounds is less than 1 ($rounds), assuming 1." @@ -87,24 +93,29 @@ class MatchmakingFragment : Fragment() { } else { queueForNewGame(mm, rounds!!) } - MatchmakingFragmentDirections.actionMatchmakingFragmentToGameFragment(gameId) - .also { findNavController().navigate(it) } - } catch (e: MatchmakingTimeoutException) { - displayCancelButton(mm) - write(e.message) - setLoading(false) - } catch (e: TimeoutCancellationException) { - displayCancelButton(mm) - write("Timed out: ${e.message}") - setLoading(false) + write(getString(R.string.ready_to_play)) + if (!isTest()) + findNavController().navigate( + MatchmakingFragmentDirections.actionMatchmakingFragmentToGameFragment( + gameId + ) + ) } catch (e: Exception) { displayCancelButton(mm) - write("Error: ${e.message}") - Log.e(null, null, e) + write(exceptionToString(e)) setLoading(false) } } + companion object { + fun exceptionToString(e: Exception): String { + return when (e) { + is MatchmakingTimeoutException -> return e.message + is TimeoutCancellationException -> "Timed out: ${e.message}" + else -> "Error: ${e.message}" + } + } + } class MatchmakingTimeoutException( val action: String, @@ -140,6 +151,7 @@ class MatchmakingFragment : Fragment() { write("Queued for game ${it.gameMode}") wait() } + is QueueStatus.GameJoined -> { } } diff --git a/app/src/main/java/ch/epfl/sweng/rps/utils/Utils.kt b/app/src/main/java/ch/epfl/sweng/rps/utils/Utils.kt index f5c974ca..8f735a8c 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/utils/Utils.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/utils/Utils.kt @@ -10,6 +10,7 @@ import android.graphics.Color import android.util.Log import android.view.View import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import com.google.firebase.auth.FirebaseAuth @@ -307,5 +308,9 @@ sealed class Option { } } +inline fun Fragment.getActivityOfType(): T? = activity?.takeIf { it is T } as? T +inline fun Fragment.requireActivityOfType(): T = getActivityOfType()!! + +const val TEST_MODE = "test_mode" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd146250..0feda9f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ Rock Paper Scissors - 896605409351-a1hlsfr6dkigthpbeul0ug46c8774uoa.apps.googleusercontent.com + 896605409351-a1hlsfr6dkigthpbeul0ug46c8774uoa.apps.googleusercontent.com + main_activity Android Studio android.studio@android.com @@ -15,9 +16,9 @@ rock image paper image scissors image - + username - + 3:1 Game LoginActivity @@ -39,7 +40,7 @@ Friends FRIENDS - + @@ -127,4 +128,5 @@ Dump debug infos dump_sys_settings Rock Paper Scissors + Ready to play! \ No newline at end of file From 2833c91fd6605a6b918df3ea20e8a68f8b8b5321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20S?= Date: Fri, 10 Jun 2022 02:44:51 +0200 Subject: [PATCH 2/3] fix small typo --- .../java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt b/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt index c7c220a9..5fc7834c 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/ui/game/MatchmakingFragment.kt @@ -63,10 +63,7 @@ class MatchmakingFragment : Fragment() { if (loading) View.VISIBLE else View.GONE } - private fun isTest(): Boolean { - requireActivity() - return requireActivity().intent.getBooleanExtra(TEST_MODE, false) - } + private fun isTest(): Boolean = requireActivity().intent.getBooleanExtra(TEST_MODE, false) private suspend fun startMatchmaking() { val mm = ServiceLocator.getInstance().matchmakingService From 81316731eed77ae7591c81287d1a912c49b86191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20S?= Date: Fri, 10 Jun 2022 04:46:18 +0200 Subject: [PATCH 3/3] absolute madlad --- .../java/ch/epfl/sweng/rps/FirebaseTests.kt | 203 +++++++++++++++--- .../ch/epfl/sweng/rps/models/remote/Game.kt | 12 ++ .../sweng/rps/remote/FirebaseRepository.kt | 18 +- .../remote/games/FirebaseGamesRepository.kt | 11 +- 4 files changed, 199 insertions(+), 45 deletions(-) diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/FirebaseTests.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/FirebaseTests.kt index 9fc4f422..208ffd2d 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/FirebaseTests.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/FirebaseTests.kt @@ -2,8 +2,10 @@ package ch.epfl.sweng.rps import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import ch.epfl.sweng.rps.models.remote.Hand +import ch.epfl.sweng.rps.models.remote.* import ch.epfl.sweng.rps.remote.Env +import ch.epfl.sweng.rps.remote.FirebaseReferences +import ch.epfl.sweng.rps.remote.FirebaseRepository import ch.epfl.sweng.rps.remote.Repository import ch.epfl.sweng.rps.services.FirebaseGameService import ch.epfl.sweng.rps.services.GameService.GameServiceException @@ -11,9 +13,14 @@ import ch.epfl.sweng.rps.services.ProdServiceLocator import ch.epfl.sweng.rps.services.ServiceLocator import ch.epfl.sweng.rps.utils.FirebaseEmulatorsUtils import ch.epfl.sweng.rps.utils.europeWest1 +import com.google.android.gms.tasks.Tasks import com.google.firebase.FirebaseApp import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.firestore.* +import com.google.firebase.firestore.ktx.toObject import com.google.firebase.ktx.Firebase +import io.mockk.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.junit.After @@ -33,7 +40,8 @@ import org.junit.runner.RunWith @ExperimentalCoroutinesApi @RunWith(AndroidJUnit4::class) class FirebaseTests { - lateinit var db: Repository + private lateinit var db: Repository + @Before fun setUp() { @@ -49,6 +57,7 @@ class FirebaseTests { @After fun tearDown() { FirebaseApp.clearInstancesForTest() + unmockkAll() } @Test @@ -65,9 +74,145 @@ class FirebaseTests { assertEquals(Env.Test, serviceLocatorDev.env) assertEquals( - serviceLocatorProd.repository, - serviceLocatorProd.repository + serviceLocatorProd.repository, serviceLocatorProd.repository + ) + } + + inline fun mockQuerySnapshot(data: Map>>): QuerySnapshot { + val querySnapshot = mockk() + every { querySnapshot.toObjects(any>()) } returns data.values.map { it.first } + every { querySnapshot.documents } returns data.map { e -> + mockk().apply { + every { this@apply.data } returns e.value.second + every { toObject(any>()) } returns e.value.first + every { toObject() } returns e.value.first + every { id } returns e.key + val k = slot() + every { this@apply.get(capture(k)) } answers { + e.value.second[k.captured] + } + } + } + return querySnapshot + } + + inline fun mockDocumentSnapshot( + id: String, data: Pair> + ): DocumentSnapshot { + val documentSnapshot = mockk() + every { documentSnapshot.data } returns data.second + val k = slot() + every { documentSnapshot.get(capture(k)) } answers { + data.second[k.captured] + } + every { documentSnapshot.toObject(any>()) } returns data.first + every { documentSnapshot.toObject() } returns data.first + every { documentSnapshot.id } returns id + return documentSnapshot + } + + fun mockTask(value: T) = Tasks.forResult(value) + inline fun mockCollection( + id: String, data: Map>> + ): CollectionReference { + val mock = mockk() + every { mock.get() } returns mockTask(mockQuerySnapshot(data)) + every { mock.id } returns id + val identities = listOf Query>( + { mock.whereArrayContains(any(), any()) }, + { mock.whereEqualTo(any(), any()) }, + { mock.whereGreaterThan(any(), any()) }, + { mock.whereGreaterThanOrEqualTo(any(), any()) }, + { mock.whereLessThan(any(), any()) }, + { mock.whereLessThanOrEqualTo(any(), any()) }, + { mock.whereNotEqualTo(any(), any()) }, + { mock.orderBy(any(), any()) }, + { mock.orderBy(any()) }, + { mock.limit(any()) }, + ) + identities.forEach { every(it) returns mock } + val s = slot() + every { mock.document(capture(s)) } answers { + mockk().apply { + every { this@apply.id } returns s.captured + every { get() } returns mockTask(mockDocumentSnapshot(s.captured, data[s.captured]!!)) + } + } + return mock + } + + /* + * class FirebaseReferences { + private val root = FirebaseFirestore.getInstance() + private val storageRoot = FirebaseStorage.getInstance().reference + + /** + * Thefriend request collection. + */ + val usersFriendRequest = root.collection("friend_requests") + + /** + * Theusers collection. + */ + val usersCollection = root.collection("users") + + /** + * Theprofile pictures folder. + */ + val profilePicturesFolder = storageRoot.child("profile_pictures") + + /** + * Thegames collection. + */ + val gamesCollection = root.collection("games") + + /** + * Thescores collection. + */ + val scoresCollection = root.collection("scores") + + /** + * Returns the collection of invitations for the given user + * with [uid]. + */ + fun invitationsOfUid(uid: String) = usersCollection + .document(uid) + .collection("invitations") +} +*/ + fun mockFirebaseReferences(): FirebaseReferences { + val m = mockk() + every { m.usersFriendRequest } returns mockCollection( + "friend_requests", mapOf("" to (FriendRequest(users = listOf("me", "you")) to mapOf())) + ) + every { m.usersCollection } returns mockCollection("users", mapOf("" to (User() to mapOf()))) + every { m.gamesCollection } returns mockCollection( + "games", mapOf( + "rps" to (Game.Rps() to mapOf( + "edition" to GameMode.GameEdition.RockPaperScissors.name, + )) + ) ) + every { m.scoresCollection } returns mockCollection("scores", mapOf("" to (TotalScore() to mapOf()))) + every { m.profilePicturesFolder } returns mockk() + return m + } + + @Test + fun testFirebaseRepo(): Unit = runBlocking { + val fb = mockFirebaseReferences() + val auth = mockk() + every { auth.currentUser } returns mockk().apply { every { uid } returns "me" } + val repo = FirebaseRepository.createInstance(fb, auth) + + assertEquals(User(), repo.getUser("").getOrThrow()) + assertEquals(Game.Rps(), repo.games.getGame("rps")) + assertEquals(listOf(Game.Rps()), repo.games.gamesOfUser("me")) + assertEquals(listOf(Game.Rps()), repo.games.myActiveGames()) + assertEquals(listOf(TotalScore()), repo.games.getLeaderBoardScore("")) + assertEquals("me", repo.getCurrentUid()) + assertEquals(listOf(FriendRequest(users = listOf("me", "you"))), repo.friends.listFriendRequests()) + assertEquals(listOf("you"), repo.friends.getFriends()) } @Test @@ -76,20 +221,17 @@ class FirebaseTests { assertEquals("1234", serviceLocatorProd.getGameServiceForGame("1234", start = false).gameId) assertTrue( serviceLocatorProd.getGameServiceForGame( - "1", - start = false + "1", start = false ) === serviceLocatorProd.getGameServiceForGame("1", start = false) ) - val service: FirebaseGameService = - serviceLocatorProd.getGameServiceForGame("1234", start = false) + val service: FirebaseGameService = serviceLocatorProd.getGameServiceForGame("1234", start = false) - val throwingActions: List Unit> = - listOf( - { it.refreshGame() }, - { it.addRound() }, - { it.playHand(Hand.PAPER) }, - ) + val throwingActions: List Unit> = listOf( + { it.refreshGame() }, + { it.addRound() }, + { it.playHand(Hand.PAPER) }, + ) for (action in throwingActions) { assertThrows(Exception::class.java) { @@ -107,8 +249,7 @@ class FirebaseTests { @Test fun disposingGameServices() { val serviceLocator = ServiceLocator.getInstance(env = Env.Prod) as ProdServiceLocator - val service: FirebaseGameService = - serviceLocator.getGameServiceForGame("1234", start = false) + val service: FirebaseGameService = serviceLocator.getGameServiceForGame("1234", start = false) assertEquals(listOf("1234"), serviceLocator.cachedGameServices) assertFalse(service.isDisposed) serviceLocator.disposeAllGameServices() @@ -120,8 +261,7 @@ class FirebaseTests { fun disposingGameServices2() { val serviceLocator = ServiceLocator.getInstance(env = Env.Prod) as ProdServiceLocator serviceLocator.disposeAllGameServices() - val service: FirebaseGameService = - serviceLocator.getGameServiceForGame("1234", start = false) + val service: FirebaseGameService = serviceLocator.getGameServiceForGame("1234", start = false) assertEquals(listOf("1234"), serviceLocator.cachedGameServices) assertFalse(service.isDisposed) service.dispose() @@ -134,20 +274,18 @@ class FirebaseTests { @Test fun usingAfterDisposedThrows() { val serviceLocator = ServiceLocator.getInstance(env = Env.Prod) as ProdServiceLocator - val service: FirebaseGameService = - serviceLocator.getGameServiceForGame("1234", start = false) + val service: FirebaseGameService = serviceLocator.getGameServiceForGame("1234", start = false) service.dispose() assertTrue(service.isDisposed) - val throwingActions: List Unit> = - listOf( - { it.refreshGame() }, - { it.addRound() }, - { it.playHand(Hand.PAPER) }, - { it.currentRound }, - { it.currentGame }, - { it.startListening() }, - { it.stopListening() }, - ) + val throwingActions: List Unit> = listOf( + { it.refreshGame() }, + { it.addRound() }, + { it.playHand(Hand.PAPER) }, + { it.currentRound }, + { it.currentGame }, + { it.startListening() }, + { it.stopListening() }, + ) for (action in throwingActions) { assertThrows(GameServiceException::class.java) { @@ -162,9 +300,8 @@ class FirebaseTests { } @Test - fun firebaseReferences() { - val firebase = - (ServiceLocator.getInstance(env = Env.Prod) as ProdServiceLocator).firebaseReferences + fun testFirebaseReferences() { + val firebase = (ServiceLocator.getInstance(env = Env.Prod) as ProdServiceLocator).firebaseReferences assertEquals("games", firebase.gamesCollection.path) assertEquals("/profile_pictures", firebase.profilePicturesFolder.path) diff --git a/app/src/main/java/ch/epfl/sweng/rps/models/remote/Game.kt b/app/src/main/java/ch/epfl/sweng/rps/models/remote/Game.kt index 12b46d2c..14b8937d 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/models/remote/Game.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/models/remote/Game.kt @@ -1,8 +1,10 @@ package ch.epfl.sweng.rps.models.remote +import androidx.annotation.VisibleForTesting import ch.epfl.sweng.rps.models.remote.GameMode.GameEdition import com.google.firebase.Timestamp import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.QuerySnapshot import java.util.* @@ -90,5 +92,15 @@ sealed class Game { } return document.toObject(type) } + + @VisibleForTesting + fun QuerySnapshot.toListOfGames(): List { + return this.documents.map { Game.fromDocumentSnapshot(it)!! } + } + + @VisibleForTesting + fun DocumentSnapshot.toGame(): Game? { + return Game.fromDocumentSnapshot(this) + } } } diff --git a/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseRepository.kt b/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseRepository.kt index dfd3149c..38c15e8f 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseRepository.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseRepository.kt @@ -3,6 +3,7 @@ package ch.epfl.sweng.rps.remote import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import androidx.annotation.VisibleForTesting import ch.epfl.sweng.rps.models.remote.User import ch.epfl.sweng.rps.remote.friends.FirebaseFriendsRepository import ch.epfl.sweng.rps.remote.friends.FriendsRepository @@ -26,7 +27,8 @@ import java.net.URL * It is used to communicate with the Firebase database. */ class FirebaseRepository private constructor( - internal val firebase: FirebaseReferences + internal val firebase: FirebaseReferences, + internal val auth: FirebaseAuth, ) : Repository { @@ -45,8 +47,7 @@ class FirebaseRepository private constructor( } override suspend fun getUser(uid: String): SuspendResult = guardSuspendable { - val user = firebase.usersCollection.document(uid).get().await() - user?.toObject() + firebase.usersCollection.document(uid).get().await()?.toObject() } override suspend fun getUserProfilePictureUrl(uid: String): SuspendResult = @@ -91,14 +92,17 @@ class FirebaseRepository private constructor( user } - override fun rawCurrentUid(): String? = FirebaseAuth.getInstance().currentUser?.uid + override fun rawCurrentUid(): String? = auth.currentUser?.uid private fun Uri.toURI(): URI = URI(toString()) private operator fun List.div(el: T): T = first { it != el } companion object { - internal fun createInstance(firebaseReferences: FirebaseReferences): FirebaseRepository { - return FirebaseRepository(firebaseReferences) - } + @VisibleForTesting + internal fun createInstance( + firebaseReferences: FirebaseReferences, + auth: FirebaseAuth = FirebaseAuth.getInstance() + ): FirebaseRepository = + FirebaseRepository(firebaseReferences, auth) } } diff --git a/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt b/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt index 87950d79..4b777662 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt @@ -1,12 +1,13 @@ package ch.epfl.sweng.rps.remote.games import ch.epfl.sweng.rps.models.remote.Game +import ch.epfl.sweng.rps.models.remote.Game.Companion.toGame +import ch.epfl.sweng.rps.models.remote.Game.Companion.toListOfGames import ch.epfl.sweng.rps.models.remote.Invitation import ch.epfl.sweng.rps.models.remote.TotalScore import ch.epfl.sweng.rps.models.remote.UserStats import ch.epfl.sweng.rps.remote.FirebaseRepository import ch.epfl.sweng.rps.utils.toListOf -import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.Query import com.google.firebase.firestore.ktx.toObject import kotlinx.coroutines.tasks.await @@ -19,10 +20,10 @@ class FirebaseGamesRepository(internal val repository: FirebaseRepository) : Gam private val firebase get() = repository.firebase override suspend fun getGame(gameId: String): Game? { - val doc: DocumentSnapshot = firebase.gamesCollection.document(gameId).get().await() - return Game.fromDocumentSnapshot(doc) + return firebase.gamesCollection.document(gameId).get().await().toGame() } + override suspend fun getLeaderBoardScore(scoreMode: String): List { return firebase.scoresCollection.orderBy(scoreMode, Query.Direction.DESCENDING).get() .await().documents.map { @@ -34,14 +35,14 @@ class FirebaseGamesRepository(internal val repository: FirebaseRepository) : Gam override suspend fun gamesOfUser(uid: String): List { return firebase.gamesCollection.whereArrayContains(Game.FIELDS.PLAYERS, uid).get() - .await().documents.map { Game.fromDocumentSnapshot(it)!! } + .await().toListOfGames() } override suspend fun myActiveGames(): List { return firebase.gamesCollection .whereArrayContains(Game.FIELDS.PLAYERS, repository.getCurrentUid()) .whereEqualTo(Game.FIELDS.DONE, false) - .get().await().documents.map { Game.fromDocumentSnapshot(it)!! } + .get().await().toListOfGames() } override suspend fun statsOfUser(uid: String): UserStats {