mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-05 18:30:59 +01:00
Fix Next Up channel launch sign in (#1886)
## Description When parsing intents, not every path for authentication was checked, so launching via the "Next Up" OS channel wouldn't check for a profile PIN. This PR fixes that. Additionally, the "Next Up" OS channel launch wasn't checking the correct parameter to ensure the back stack was written properly. ### Related issues Fixes #1883 ### Testing I manually test on the emulator with these test cases: - No profile PIN, cold start - No profile PIN, hot start - Profile PIN, cold start - Profile PIN, hot start - No profile PIN, launch via "Next Up" channel - No profile PIN, launch via intent - Profile PIN, launch via "Next Up" channel - Profile PIN, launch via intent Also added unit tests for intent-based and regular launching which covers those cases and more. ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
39523aa6e2
commit
4b927e6b7b
5 changed files with 736 additions and 34 deletions
|
|
@ -439,9 +439,16 @@ class MainActivityViewModel
|
|||
val result = intent?.let { intentService.parseIntent(intent) }
|
||||
when (result) {
|
||||
is IntentResult.Error -> {
|
||||
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
||||
val current = serverRepository.current.value
|
||||
val destination =
|
||||
if (current != null) {
|
||||
SetupDestination.UserList(current.server)
|
||||
} else {
|
||||
SetupDestination.ServerList
|
||||
}
|
||||
setupNavigationManager.navigateTo(destination)
|
||||
Timber.e("Error parsing intent: %s", result.message)
|
||||
showToast(context, "Invalid intent: ${result.message}")
|
||||
showToast(context, result.message)
|
||||
return@withLock
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +512,7 @@ class MainActivityViewModel
|
|||
prefs.currentUserId?.toUUIDOrNull(),
|
||||
)
|
||||
if (current != null) {
|
||||
if (current.user.hasPin || current.user.requireLogin) {
|
||||
if (current.user.isProtected) {
|
||||
setupNavigationManager.navigateTo(
|
||||
SetupDestination.UserList(
|
||||
current.server,
|
||||
|
|
|
|||
|
|
@ -29,49 +29,26 @@ class IntentService
|
|||
Timber.v("Intent data: %s", intent.data)
|
||||
val action = intent.action ?: intent.data?.host
|
||||
if (
|
||||
intent.getStringParam("type") == null &&
|
||||
intent.getStringParam(INTENT_ITEM_TYPE) == null &&
|
||||
(action == Intent.ACTION_MAIN || action.isNullOrBlank())
|
||||
) {
|
||||
// Normal app launch
|
||||
return IntentResult.NoOp
|
||||
}
|
||||
|
||||
val userId = intent.getStringParam("userId")?.toUUIDOrNull()
|
||||
val serverId = intent.getStringParam("serverId")?.toUUIDOrNull()
|
||||
if (userId != null && serverId != null) {
|
||||
Timber.v("Intent switches user")
|
||||
val user =
|
||||
serverRepository.serverDao.getUser(serverId, userId)
|
||||
?: return IntentResult.Error("User not found")
|
||||
if (!user.isProtected) {
|
||||
serverRepository.restoreSession(serverId, userId)
|
||||
} else {
|
||||
return IntentResult.Error("Cannot switch to specified user")
|
||||
}
|
||||
} else if (serverRepository.currentUser == null) {
|
||||
Timber.v("No current user, so restoring last")
|
||||
val appPrefs = userPreferencesService.flow.first().appPreferences
|
||||
if (appPrefs.signInAutomatically) {
|
||||
val userId = appPrefs.currentUserId.toUUIDOrNull()
|
||||
val serverId = appPrefs.currentServerId.toUUIDOrNull()
|
||||
if (userId != null && serverId != null) {
|
||||
serverRepository.restoreSession(serverId, userId)
|
||||
?: return IntentResult.Error("Error restoring user")
|
||||
} else {
|
||||
return IntentResult.Error("Could not auto sign-in, specify a server & user")
|
||||
}
|
||||
} else {
|
||||
return IntentResult.Error("Auto sign-in not enabled, specify a server & user")
|
||||
}
|
||||
val result = prepare(intent)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (intent.getStringParam("type") != null) {
|
||||
// Existence of this key implies launching from Play Next channel
|
||||
if (intent.getStringParam(INTENT_ITEM_TYPE) != null) {
|
||||
return getDestinationFromChannel(intent)?.let {
|
||||
IntentResult.Target(
|
||||
destinations = listOf(it),
|
||||
addHomeToBackStack = false,
|
||||
)
|
||||
}
|
||||
?: IntentResult.Error("Invalid parameters")
|
||||
} ?: IntentResult.Error("Invalid parameters")
|
||||
}
|
||||
|
||||
if (action == Intent.ACTION_SEARCH || action == "search") {
|
||||
|
|
@ -130,6 +107,49 @@ class IntentService
|
|||
return IntentResult.Target(destinations)
|
||||
}
|
||||
|
||||
internal suspend fun prepare(intent: Intent): IntentResult? {
|
||||
val appPrefs = userPreferencesService.flow.first().appPreferences
|
||||
|
||||
val userId = intent.getStringParam(INTENT_USER_ID)?.toUUIDOrNull()
|
||||
val serverId = intent.getStringParam(INTENT_SERVER_ID)?.toUUIDOrNull()
|
||||
return if (userId != null && serverId != null) {
|
||||
Timber.v("Intent switches user")
|
||||
val user = serverRepository.serverDao.getUser(serverId, userId)
|
||||
if (user != null && !user.isProtected) {
|
||||
serverRepository.restoreSession(serverId, userId)
|
||||
?: IntentResult.Error("Error restoring user")
|
||||
null
|
||||
} else {
|
||||
IntentResult.Error("Cannot switch to specified user")
|
||||
}
|
||||
} else {
|
||||
val profileProtected =
|
||||
serverRepository.current.value
|
||||
?.user
|
||||
?.isProtected == true
|
||||
if (appPrefs.signInAutomatically && !profileProtected) {
|
||||
Timber.v("No current user, so restoring last")
|
||||
val userId = appPrefs.currentUserId?.toUUIDOrNull()
|
||||
val serverId = appPrefs.currentServerId?.toUUIDOrNull()
|
||||
|
||||
if (userId != null && serverId != null) {
|
||||
val current =
|
||||
serverRepository.restoreSession(serverId, userId)
|
||||
?: return IntentResult.Error("Error restoring user")
|
||||
if (current.user.isProtected) {
|
||||
IntentResult.Error("Could not auto sign-in, user is protected")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
IntentResult.Error("Could not auto sign-in, specify a server & user")
|
||||
}
|
||||
} else {
|
||||
IntentResult.Error("Auto sign-in not enabled or user is protected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Intent.getStringParam(key: String) = getStringExtra(key) ?: data?.getQueryParameter(key)
|
||||
|
||||
private fun Intent.getLongParam(key: String) =
|
||||
|
|
@ -173,6 +193,8 @@ class IntentService
|
|||
const val INTENT_EPISODE_NUMBER = "epNum"
|
||||
const val INTENT_SEASON_NUMBER = "seaNum"
|
||||
const val INTENT_SEASON_ID = "seaId"
|
||||
const val INTENT_SERVER_ID = "serverId"
|
||||
const val INTENT_USER_ID = "userId"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Intent
|
||||
import com.github.damontecres.wholphin.data.CurrentUser
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.preferences.update
|
||||
import com.github.damontecres.wholphin.test.currentUser
|
||||
import com.github.damontecres.wholphin.test.server
|
||||
import com.github.damontecres.wholphin.test.user
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class IntentServiceTest {
|
||||
private val api: ApiClient = mockk()
|
||||
private val serverRepository: ServerRepository = mockk()
|
||||
private val userPreferencesService: UserPreferencesService = mockk()
|
||||
|
||||
lateinit var intentService: IntentService
|
||||
|
||||
private val serverId = UUID.randomUUID()
|
||||
private val userId = UUID.randomUUID()
|
||||
private val currentUser = currentUser(serverId, userId)
|
||||
|
||||
private val protectedCurrentUser =
|
||||
CurrentUser(
|
||||
server(serverId),
|
||||
user(serverId, userId).copy(pin = "1234"),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
intentService = IntentService(api, serverRepository, userPreferencesService)
|
||||
}
|
||||
|
||||
private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) {
|
||||
every { userPreferencesService.flow } returns
|
||||
flow {
|
||||
emit(
|
||||
UserPreferences(
|
||||
AppPreferences.getDefaultInstance().update(block),
|
||||
null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in with unprotected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
|
||||
val intent = Intent()
|
||||
val result = intentService.prepare(intent)
|
||||
assertNull(result)
|
||||
|
||||
coVerify { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign disabled in with unprotected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = false
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
|
||||
val intent = Intent()
|
||||
val result = intentService.prepare(intent)
|
||||
assertTrue(result is IntentResult.Error)
|
||||
|
||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in, hot load, with protected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
||||
|
||||
val intent = Intent()
|
||||
val result = intentService.prepare(intent)
|
||||
assertTrue(result is IntentResult.Error)
|
||||
|
||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in, cold load, with protected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
||||
|
||||
val intent = Intent()
|
||||
val result = intentService.prepare(intent)
|
||||
assertTrue(result is IntentResult.Error)
|
||||
|
||||
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in, no current user`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = ""
|
||||
currentUserId = ""
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||
// coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
||||
|
||||
val intent = Intent()
|
||||
val result = intentService.prepare(intent)
|
||||
assertTrue(result is IntentResult.Error)
|
||||
|
||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test user specified with unprotected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = ""
|
||||
currentUserId = ""
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns currentUser.user
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
|
||||
val intent =
|
||||
Intent().apply {
|
||||
putExtra(IntentService.INTENT_SERVER_ID, serverId.toServerString())
|
||||
putExtra(IntentService.INTENT_USER_ID, userId.toServerString())
|
||||
}
|
||||
val result = intentService.prepare(intent)
|
||||
assertNull(result)
|
||||
|
||||
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test user specified with protected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = ""
|
||||
currentUserId = ""
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns protectedCurrentUser.user
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
||||
|
||||
val intent =
|
||||
Intent().apply {
|
||||
putExtra(IntentService.INTENT_SERVER_ID, serverId.toServerString())
|
||||
putExtra(IntentService.INTENT_USER_ID, userId.toServerString())
|
||||
}
|
||||
val result = intentService.prepare(intent)
|
||||
assertTrue(result is IntentResult.Error)
|
||||
|
||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test user specified does not exist`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = ""
|
||||
currentUserId = ""
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns null
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns null
|
||||
|
||||
val intent =
|
||||
Intent().apply {
|
||||
putExtra(IntentService.INTENT_SERVER_ID, serverId.toServerString())
|
||||
putExtra(IntentService.INTENT_USER_ID, userId.toServerString())
|
||||
}
|
||||
val result = intentService.prepare(intent)
|
||||
assertTrue(result is IntentResult.Error)
|
||||
|
||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
package com.github.damontecres.wholphin.test
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.github.damontecres.wholphin.MainActivityViewModel
|
||||
import com.github.damontecres.wholphin.data.CurrentUser
|
||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.update
|
||||
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.DeviceProfileService
|
||||
import com.github.damontecres.wholphin.services.IntentResult
|
||||
import com.github.damontecres.wholphin.services.IntentService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||
import com.github.damontecres.wholphin.util.configure
|
||||
import com.github.damontecres.wholphin.util.reset
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TestMainActivityViewModel {
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
private val context: Context = mockk()
|
||||
private val preferences: DataStore<AppPreferences> = mockk()
|
||||
private val serverRepository: ServerRepository = mockk()
|
||||
private val setupNavigationManager: SetupNavigationManager = mockk(relaxed = true)
|
||||
private val navigationManager: NavigationManager = mockk(relaxed = true)
|
||||
private val deviceProfileService: DeviceProfileService = mockk()
|
||||
private val backdropService: BackdropService = mockk(relaxed = true)
|
||||
private val appUpgradeHandler: AppUpgradeHandler = mockk()
|
||||
private val intentService: IntentService = mockk()
|
||||
private val serverDao: JellyfinServerDao = mockk()
|
||||
|
||||
val viewModel =
|
||||
MainActivityViewModel(
|
||||
context,
|
||||
preferences,
|
||||
serverRepository,
|
||||
setupNavigationManager,
|
||||
navigationManager,
|
||||
deviceProfileService,
|
||||
backdropService,
|
||||
appUpgradeHandler,
|
||||
intentService,
|
||||
)
|
||||
|
||||
private val serverId = UUID.randomUUID()
|
||||
private val userId = UUID.randomUUID()
|
||||
private val currentUser = currentUser(serverId, userId)
|
||||
|
||||
private val protectedCurrentUser =
|
||||
CurrentUser(
|
||||
server(serverId),
|
||||
user(serverId, userId).copy(pin = "1234"),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
WholphinDispatchers.configure(testDispatcher)
|
||||
every { appUpgradeHandler.needUpgrade() } returns false
|
||||
every { appUpgradeHandler.copySubfont(any()) } returns Unit
|
||||
every { serverRepository.serverDao } returns serverDao
|
||||
coEvery { serverDao.getServer(serverId) } returns
|
||||
JellyfinServerUsers(
|
||||
currentUser.server,
|
||||
// User list is unused
|
||||
emptyList(),
|
||||
)
|
||||
mockkStatic(Toast::class)
|
||||
val mockToast = mockk<Toast>()
|
||||
every { Toast.makeText(any(), any<CharSequence>(), any()) } returns mockToast
|
||||
every { mockToast.show() } returns Unit
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
WholphinDispatchers.reset()
|
||||
unmockkStatic(Toast::class)
|
||||
}
|
||||
|
||||
private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) {
|
||||
every { preferences.data } returns
|
||||
flow {
|
||||
emit(
|
||||
AppPreferences.getDefaultInstance().update(block),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in with unprotected profile`() =
|
||||
runTest(testDispatcher) {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
|
||||
viewModel.appStart(null)
|
||||
advanceUntilIdle()
|
||||
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
||||
destination as SetupDestination.AppContent
|
||||
assertEquals(currentUser, destination.current)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign disabled in with current server`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = false
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||
// coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
|
||||
viewModel.appStart(null)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 1) { serverDao.getServer(serverId) }
|
||||
coVerify(exactly = 0) { serverRepository.restoreSession(any(), any()) }
|
||||
val args = mutableListOf<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(args)) }
|
||||
assertEquals(2, args.size)
|
||||
args[0].let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading)
|
||||
}
|
||||
args[1].let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
||||
destination as SetupDestination.UserList
|
||||
assertEquals(currentUser.server, destination.server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in, hot load, with protected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
||||
|
||||
viewModel.appStart(null)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
||||
verify { serverDao.getServer(serverId) }
|
||||
val args = mutableListOf<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(args)) }
|
||||
assertEquals(2, args.size)
|
||||
args[0].let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading)
|
||||
}
|
||||
args[1].let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
||||
destination as SetupDestination.UserList
|
||||
assertEquals(currentUser.server, destination.server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in, cold load, with protected profile`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
||||
|
||||
viewModel.appStart(null)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
||||
destination as SetupDestination.UserList
|
||||
assertEquals(currentUser.server, destination.server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test auto sign in, no current user`() =
|
||||
runTest {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = ""
|
||||
currentUserId = ""
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||
coEvery { serverRepository.restoreSession(null, null) } returns null
|
||||
|
||||
viewModel.appStart(null)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { serverRepository.restoreSession(null, null) }
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test intent result is noop`() =
|
||||
runTest(testDispatcher) {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
coEvery { intentService.parseIntent(any()) } returns IntentResult.NoOp
|
||||
|
||||
viewModel.appStart(Intent())
|
||||
advanceUntilIdle()
|
||||
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
||||
destination as SetupDestination.AppContent
|
||||
assertEquals(currentUser, destination.current)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test intent result is error, has server`() =
|
||||
runTest(testDispatcher) {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
|
||||
|
||||
viewModel.appStart(Intent())
|
||||
advanceUntilIdle()
|
||||
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
||||
destination as SetupDestination.UserList
|
||||
assertEquals(currentUser.server, destination.server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test intent result is error, does not have server`() =
|
||||
runTest(testDispatcher) {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow(null)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
|
||||
|
||||
viewModel.appStart(Intent())
|
||||
advanceUntilIdle()
|
||||
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test intent result is target, has server`() =
|
||||
runTest(testDispatcher) {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
coEvery { intentService.parseIntent(any()) } returns
|
||||
IntentResult.Target(
|
||||
destinations = listOf(Destination.Favorites),
|
||||
addHomeToBackStack = true,
|
||||
)
|
||||
val backstack = mutableListOf<Destination>()
|
||||
every { navigationManager.backStack } returns backstack
|
||||
|
||||
viewModel.appStart(Intent())
|
||||
advanceUntilIdle()
|
||||
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
verify { navigationManager.reloadHome() }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
||||
destination as SetupDestination.AppContent
|
||||
assertEquals(currentUser, destination.current)
|
||||
}
|
||||
// Note: navigation manager internally handles Home destination, so it won't be in this list
|
||||
assertEquals(1, backstack.size)
|
||||
backstack[0].let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is Destination.Favorites)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test intent result is target, has server, no home backstack`() =
|
||||
runTest(testDispatcher) {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
coEvery { intentService.parseIntent(any()) } returns
|
||||
IntentResult.Target(
|
||||
destinations = listOf(Destination.Favorites),
|
||||
addHomeToBackStack = false,
|
||||
)
|
||||
val backstack = mutableListOf<Destination>()
|
||||
every { navigationManager.backStack } returns backstack
|
||||
|
||||
viewModel.appStart(Intent())
|
||||
advanceUntilIdle()
|
||||
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
verify(exactly = 0) { navigationManager.reloadHome() }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
||||
destination as SetupDestination.AppContent
|
||||
assertEquals(currentUser, destination.current)
|
||||
}
|
||||
assertEquals(1, backstack.size)
|
||||
backstack[0].let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is Destination.Favorites)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test intent result is target, has no server`() =
|
||||
runTest(testDispatcher) {
|
||||
setupPreferences {
|
||||
signInAutomatically = true
|
||||
currentServerId = serverId.toServerString()
|
||||
currentUserId = userId.toServerString()
|
||||
}
|
||||
every { serverRepository.current } returns MutableStateFlow(null)
|
||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||
coEvery { intentService.parseIntent(any()) } returns
|
||||
IntentResult.Target(
|
||||
destinations = listOf(Destination.Favorites),
|
||||
addHomeToBackStack = true,
|
||||
)
|
||||
val backstack = mutableListOf<Destination>()
|
||||
every { navigationManager.backStack } returns backstack
|
||||
|
||||
viewModel.appStart(Intent())
|
||||
advanceUntilIdle()
|
||||
|
||||
val slot = slot<SetupDestination>()
|
||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||
verify(exactly = 0) { navigationManager.reloadHome() }
|
||||
slot.captured.let { destination ->
|
||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
package com.github.damontecres.wholphin.test
|
||||
|
||||
import com.github.damontecres.wholphin.data.CurrentUser
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import io.mockk.MockKMatcherScope
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.NameGuidPair
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
|
||||
fun MockKMatcherScope.nonBlankString() = match<String> { it.isNotNullOrBlank() }
|
||||
|
||||
|
|
@ -75,3 +79,35 @@ fun playlist(
|
|||
seriesId = null,
|
||||
genreItems = genres,
|
||||
)
|
||||
|
||||
fun server(serverId: UUID = UUID.randomUUID()) = JellyfinServer(serverId, "test server", "http://localhost:8096", "10.11.11")
|
||||
|
||||
fun user(
|
||||
serverId: UUID,
|
||||
userId: UUID = UUID.randomUUID(),
|
||||
) = JellyfinUser(
|
||||
rowId = 1,
|
||||
id = userId,
|
||||
serverId = serverId,
|
||||
name = "test-user",
|
||||
accessToken = "token",
|
||||
pin = null,
|
||||
)
|
||||
|
||||
fun userDto(userId: UUID) =
|
||||
UserDto(
|
||||
id = userId,
|
||||
name = "test-user",
|
||||
serverName = "test server",
|
||||
hasPassword = true,
|
||||
hasConfiguredPassword = true,
|
||||
hasConfiguredEasyPassword = false,
|
||||
)
|
||||
|
||||
fun currentUser(
|
||||
serverId: UUID = UUID.randomUUID(),
|
||||
userId: UUID = UUID.randomUUID(),
|
||||
) = CurrentUser(
|
||||
server(serverId),
|
||||
user(userId),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue