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:
Damontecres 2026-08-21 15:04:07 -04:00 • committed by GitHub
parent 39523aa6e2
commit 4b927e6b7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 736 additions and 34 deletions

View file

@ -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,

View file

@ -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"
}
}

View file

@ -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) }
}
}

View file

@ -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)
}
}
}

View file

@ -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),
)