Updates to session restoration (#1905)

## Description
This PR reworks some of the internal handling of changing users and auto
sign in.

1. Most recently used server/user is migrated from `AppPreferences` to
`MostRecentServerProvider`
2. Checking if a user profile is protected occurs at a lower level in
`ServerRepository` so fewer services need to check for this
3. Now always defaults to the most recent server's user list in cases
where the most recent user can't be switched to such as if the profile
is protected, auto signin is disabled, or and error occurred

The default implementation for `MostRecentServerProvider` uses Android's
`SharedPreferences`. `SharedPreferences` was already was being used to
track the most recent server for crash reporting (the crash reporting
process cannot use the Hilt DI). This PR enhances this and adds a better
API for saving/retrieving.

This also removes the last bit of "user specific" data from
`AppPreferences`.

### Related issues
None specific, but there have been a few related to auto sign-in &
intents that were recently fixed. This PR is a more detailed approach to
those same problems.

### Testing
Manually via the emulator. There are also unit tests for most (all?) of
the different settings and state combinations.

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-10-11 20:11:09 -04:00 • committed by GitHub
parent a11cd9fb74
commit f01c5d367c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 786 additions and 493 deletions

View file

@ -1,14 +1,12 @@
package com.github.damontecres.wholphin.test
package com.github.damontecres.wholphin
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.RestoredSession
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
@ -19,8 +17,10 @@ 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.test.currentUser
import com.github.damontecres.wholphin.test.server
import com.github.damontecres.wholphin.test.user
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
@ -36,11 +36,11 @@ 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
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@ -58,7 +58,6 @@ class TestMainActivityViewModel {
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(
@ -74,6 +73,7 @@ class TestMainActivityViewModel {
)
private val serverId = UUID.randomUUID()
private val server = server(serverId)
private val userId = UUID.randomUUID()
private val currentUser = currentUser(serverId, userId)
@ -88,13 +88,6 @@ class TestMainActivityViewModel {
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
@ -121,22 +114,26 @@ class TestMainActivityViewModel {
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 { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
// coEvery { serverRepository.restoreLastSession() } returns
// RestoredSession.Success(currentUser)
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.AppContent,
)
destination as SetupDestination.AppContent
assertEquals(currentUser, destination.current)
Assert.assertEquals(currentUser, destination.current)
}
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
coVerify(exactly = 0) { serverRepository.getMostRecentServer() }
}
@Test
@ -144,27 +141,27 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = false
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
// coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
coEvery { serverRepository.getMostRecentServer() } returns
RestoredSession.ServerOnly(server)
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
verify(exactly = 1) { serverDao.getServer(serverId) }
coVerify(exactly = 0) { serverRepository.restoreSession(any(), any()) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(any(), any()) }
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
coVerify(exactly = 1) { serverRepository.getMostRecentServer() }
val args = mutableListOf<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(args)) }
assertEquals(2, args.size)
Assert.assertEquals(1, 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)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.UserList,
)
destination as SetupDestination.UserList
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -173,27 +170,30 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
every { serverRepository.current } returns
MutableStateFlow<CurrentUser?>(
protectedCurrentUser,
)
coEvery {
serverRepository.tryChangeUser(serverId, userId)
} returns null
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.ServerOnly(server)
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
verify { serverDao.getServer(serverId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
val args = mutableListOf<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(args)) }
assertEquals(2, args.size)
Assert.assertEquals(1, 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)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.UserList,
)
destination as SetupDestination.UserList
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -202,22 +202,26 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.ServerOnly(server)
coEvery {
serverRepository.tryChangeUser(serverId, userId)
} returns null
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.UserList,
)
destination as SetupDestination.UserList
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -226,45 +230,49 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = ""
currentUserId = ""
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.restoreSession(null, null) } returns null
coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
coVerify(exactly = 1) { serverRepository.restoreSession(null, null) }
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.ServerList,
)
}
}
@Test
fun `Test intent result is noop`() =
fun `Test intent result is noop, hot reload`() =
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 { serverRepository.restoreLastSession() } returns
RestoredSession.Success(currentUser)
coEvery { intentService.parseIntent(any()) } returns IntentResult.NoOp
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertFalse(result)
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)
}
// val slot = slot<SetupDestination>()
// verify { setupNavigationManager.navigateTo(capture(slot)) }
// slot.captured.let { destination ->
// Assert.assertTrue(
// "destination is ${destination::class}",
// destination is SetupDestination.AppContent,
// )
// destination as SetupDestination.AppContent
// Assert.assertEquals(currentUser, destination.current)
// }
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
coVerify(exactly = 0) { serverRepository.getMostRecentServer() }
}
@Test
@ -272,22 +280,22 @@ class TestMainActivityViewModel {
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 result = viewModel.handleIntent(Intent())
assertTrue(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.UserList,
)
destination as SetupDestination.UserList
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -296,20 +304,20 @@ class TestMainActivityViewModel {
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 result = viewModel.handleIntent(Intent())
assertTrue(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.ServerList,
)
}
}
@ -318,11 +326,9 @@ class TestMainActivityViewModel {
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 { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
coEvery { intentService.parseIntent(any()) } returns
IntentResult.Target(
destinations = listOf(Destination.Favorites),
@ -331,21 +337,27 @@ class TestMainActivityViewModel {
val backstack = mutableListOf<Destination>()
every { navigationManager.backStack } returns backstack
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
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)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.AppContent,
)
destination as SetupDestination.AppContent
assertEquals(currentUser, destination.current)
Assert.assertEquals(currentUser, destination.current)
}
// Note: navigation manager internally handles Home destination, so it won't be in this list
assertEquals(1, backstack.size)
Assert.assertEquals(1, backstack.size)
backstack[0].let { destination ->
assertTrue("destination is ${destination::class}", destination is Destination.Favorites)
Assert.assertTrue(
"destination is ${destination::class}",
destination is Destination.Favorites,
)
}
}
@ -354,11 +366,9 @@ class TestMainActivityViewModel {
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 { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
coEvery { intentService.parseIntent(any()) } returns
IntentResult.Target(
destinations = listOf(Destination.Favorites),
@ -367,20 +377,26 @@ class TestMainActivityViewModel {
val backstack = mutableListOf<Destination>()
every { navigationManager.backStack } returns backstack
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
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)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.AppContent,
)
destination as SetupDestination.AppContent
assertEquals(currentUser, destination.current)
Assert.assertEquals(currentUser, destination.current)
}
assertEquals(1, backstack.size)
Assert.assertEquals(1, backstack.size)
backstack[0].let { destination ->
assertTrue("destination is ${destination::class}", destination is Destination.Favorites)
Assert.assertTrue(
"destination is ${destination::class}",
destination is Destination.Favorites,
)
}
}
@ -389,11 +405,9 @@ class TestMainActivityViewModel {
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 { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
coEvery { intentService.parseIntent(any()) } returns
IntentResult.Target(
destinations = listOf(Destination.Favorites),
@ -402,14 +416,17 @@ class TestMainActivityViewModel {
val backstack = mutableListOf<Destination>()
every { navigationManager.backStack } returns backstack
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
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)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.ServerList,
)
}
}
}

View file

@ -3,7 +3,7 @@ package com.github.damontecres.wholphin.services
import android.app.SearchManager
import android.content.Intent
import com.github.damontecres.wholphin.data.CurrentUser
import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.RestoredSession
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
@ -20,7 +20,6 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
@ -42,7 +41,6 @@ import org.robolectric.annotation.Config
class IntentServiceTest {
private val api: ApiClient = mockk()
private val serverRepository: ServerRepository = mockk()
private val serverDao: JellyfinServerDao = mockk()
private val userPreferencesService: UserPreferencesService = mockk()
private val userLibraryApi: UserLibraryApi = mockk()
@ -64,7 +62,6 @@ class IntentServiceTest {
intentService = IntentService(api, serverRepository, userPreferencesService)
every { api.userLibraryApi } returns userLibraryApi
coEvery { userLibraryApi.getItem(itemId) } returns successResponse(movie(itemId))
every { serverRepository.serverDao } returns serverDao
}
private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) {
@ -82,11 +79,11 @@ class IntentServiceTest {
private fun setupAutoSignInUnprotected() {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.Success(currentUser)
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser
}
@Test
@ -98,7 +95,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertNull(result)
coVerify { serverRepository.restoreSession(serverId, userId) }
coVerify { serverRepository.restoreLastSession() }
}
@Test
@ -106,17 +103,17 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = false
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.Success(currentUser)
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser
val intent = Intent()
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -124,17 +121,17 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.ServerOnly(protectedCurrentUser.server)
val intent = Intent()
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -142,17 +139,17 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.ServerOnly(protectedCurrentUser.server)
val intent = Intent()
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
}
@Test
@ -160,17 +157,16 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = ""
currentUserId = ""
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None
// 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) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -178,12 +174,11 @@ class IntentServiceTest {
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
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.Success(currentUser)
val intent =
Intent().apply {
@ -193,7 +188,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertNull(result)
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -201,12 +196,11 @@ class IntentServiceTest {
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
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
// coEvery { serverRepository.restoreLastSession() } returns
// RestoredSession.ServerOnly(protectedCurrentUser.server)
val intent =
Intent().apply {
@ -216,7 +210,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -224,12 +218,9 @@ class IntentServiceTest {
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
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
val intent =
Intent().apply {
@ -239,7 +230,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -296,12 +287,11 @@ class IntentServiceTest {
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
coEvery {
serverRepository.restoreSession(
serverRepository.tryChangeUser(
newServerId,
newUserId,
)
} returns newCurrentUser
every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user
val intent =
Intent().apply {
@ -316,9 +306,8 @@ class IntentServiceTest {
assertEquals(0, destinations.size)
assertTrue(result.addHomeToBackStack)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
verify { serverDao.getUser(newServerId, newUserId) }
coVerify { serverRepository.restoreSession(newServerId, newUserId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
coVerify { serverRepository.tryChangeUser(newServerId, newUserId) }
coVerify(exactly = 0) { userLibraryApi.getItem(itemId) }
}
@ -333,12 +322,11 @@ class IntentServiceTest {
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
coEvery {
serverRepository.restoreSession(
serverRepository.tryChangeUser(
newServerId,
newUserId,
)
} returns newCurrentUser
every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user
val intent =
Intent().apply {
@ -358,9 +346,8 @@ class IntentServiceTest {
assertEquals(itemId, first.itemId)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
verify { serverDao.getUser(newServerId, newUserId) }
coVerify { serverRepository.restoreSession(newServerId, newUserId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
coVerify { serverRepository.tryChangeUser(newServerId, newUserId) }
coVerify { userLibraryApi.getItem(itemId) }
}

View file

@ -1,11 +1,13 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import android.content.SharedPreferences
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import com.github.damontecres.wholphin.data.CurrentUser
import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.MostRecentServer
import com.github.damontecres.wholphin.data.MostRecentServerProvider
import com.github.damontecres.wholphin.data.RestoredSession
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
@ -14,6 +16,7 @@ import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
import com.github.damontecres.wholphin.test.nonBlankString
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.successResponse
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.WholphinDispatchers
@ -21,6 +24,7 @@ import com.github.damontecres.wholphin.util.configure
import com.github.damontecres.wholphin.util.reset
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -29,7 +33,6 @@ import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
@ -48,6 +51,8 @@ import org.jellyfin.sdk.model.api.PublicSystemInfo
import org.jellyfin.sdk.model.api.UserDto
import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -80,14 +85,13 @@ class ServerRepositoryTest {
private val mockUserApi = mockk<UserApi>()
private val mockSystemApi = mockk<SystemApi>()
private val mockSharedPreferences = mockk<SharedPreferences>(relaxed = true)
private val mockMostRecentServerProvider = mockk<MostRecentServerProvider>()
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setup() {
WholphinDispatchers.configure(testDispatcher)
every { mockContext.getSharedPreferences(any(), any()) } returns mockSharedPreferences
every { mockApiClient.userApi } returns mockUserApi
every { mockApiClient.systemApi } returns mockSystemApi
coEvery { mockUserApi.getCurrentUser() } returns successResponse(userDto)
@ -102,9 +106,15 @@ class ServerRepositoryTest {
emptyMap(),
)
coEvery { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
coEvery { mockJellyfinServerDao.getServer(serverId) } returns
JellyfinServerUsers(server, listOf(user))
coEvery { mockJellyfinServerDao.addOrUpdateServer(server) } just Runs
every { mockApiClient.clientInfo } returns ClientInfo("Wholphin test", "0.0.1")
every { mockApiClient.deviceInfo } returns DeviceInfo("Wholphin test ID", "Wholphin test device")
every { mockApiClient.update(any(), any(), any(), any()) } just Runs
coEvery { mockMostRecentServerProvider.save(any()) } just Runs
coEvery { mockMostRecentServerProvider.clear() } just Runs
coEvery { mockMostRecentServerProvider.clearUser() } just Runs
}
@OptIn(ExperimentalCoroutinesApi::class)
@ -119,8 +129,8 @@ class ServerRepositoryTest {
mockJellyfin,
mockJellyfinServerDao,
mockApiClient,
dataStore,
testDispatcher,
mockMostRecentServerProvider,
)
private val serverId = UUID.randomUUID()
@ -194,24 +204,24 @@ class ServerRepositoryTest {
Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
verify(exactly = 1) { mockSharedPreferences.edit() }
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
verify(exactly = 1) { mockJellyfinServerDao.addOrUpdateUser(user) }
}
@Test
fun `Test restoreUser via changeUser`() =
runTest {
val user = user.copy(pin = null)
every { mockJellyfinServerDao.addOrUpdateServer(any()) } just Runs
every { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user))
val serverRepository = create()
Assert.assertNull(serverRepository.currentUser)
serverRepository.restoreSession(server.id, user.id)
val result = serverRepository.tryChangeUser(server.id, user.id)
assertTrue(result != null)
assertEquals(serverId, result?.server?.id)
assertEquals(user.id, result?.user?.id)
verify(exactly = 1) {
mockApiClient.update(
@ -226,34 +236,6 @@ class ServerRepositoryTest {
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
}
@Test
fun `Test restoreUser via shortcut`() =
runTest {
every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user))
val serverRepository = create()
setUpCurrentUser(serverRepository)
serverRepository.restoreSession(server.id, user.id)
verify(exactly = 1) {
mockApiClient.update(
baseUrl = server.url,
accessToken = user.accessToken,
clientInfo = any(),
deviceInfo = any(),
)
}
verify(exactly = 0) { mockJellyfinServerDao.addOrUpdateServer(any()) }
verify(exactly = 0) { mockJellyfinServerDao.addOrUpdateUser(any()) }
Assert.assertEquals(server, serverRepository.currentServer)
Assert.assertEquals(user, serverRepository.currentUser)
// TODO fix this, need to check userDto too
// Assert.assertEquals(userDto, serverRepository.currentUserDto)
}
@Test
fun `Test remove user`() =
runTest {
@ -308,6 +290,7 @@ class ServerRepositoryTest {
)
}
verify(exactly = 1) { mockJellyfinServerDao.deleteUser(serverId, userId) }
coVerify { mockMostRecentServerProvider.clearUser() }
}
@Test
@ -351,6 +334,7 @@ class ServerRepositoryTest {
)
}
verify(exactly = 1) { mockJellyfinServerDao.deleteServer(serverId) }
coVerify { mockMostRecentServerProvider.clear() }
}
@Test
@ -387,11 +371,7 @@ class ServerRepositoryTest {
Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
verify(exactly = 1) { mockSharedPreferences.edit() }
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
}
@Test
@ -430,11 +410,7 @@ class ServerRepositoryTest {
Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
verify(exactly = 1) { mockSharedPreferences.edit() }
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
}
@Test
@ -484,4 +460,99 @@ class ServerRepositoryTest {
}
}
}
@Test
fun `Test restoreLastSession, success`() =
runTest {
val user = user.copy(pin = null)
coEvery { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
coEvery { mockJellyfinServerDao.getServer(serverId) } returns
JellyfinServerUsers(server, listOf(user))
every { mockMostRecentServerProvider.get() } returns
MostRecentServer.from(CurrentUser(server, user))
val result = create().restoreLastSession()
assertTrue(result is RestoredSession.Success)
val current = (result as RestoredSession.Success).currentUser
assertEquals(server, current.server)
assertEquals(user, current.user)
verify(exactly = 1) {
mockApiClient.update(
baseUrl = server.url,
accessToken = user.accessToken,
clientInfo = any(),
deviceInfo = any(),
)
}
coVerify { mockMostRecentServerProvider.save(any()) }
}
@Test
fun `Test restoreLastSession, user protected`() =
runTest {
every { mockMostRecentServerProvider.get() } returns
MostRecentServer.ServerAndUser(serverId, server.url, userId, "token")
val result = create().restoreLastSession()
assertTrue(result is RestoredSession.ServerOnly)
val current = (result as RestoredSession.ServerOnly)
assertEquals(server, current.server)
}
@Test
fun `Test restoreLastSession, server only`() =
runTest {
every { mockMostRecentServerProvider.get() } returns
MostRecentServer.Server(server.id, server.url)
val result = create().restoreLastSession()
assertTrue(result is RestoredSession.ServerOnly)
val current = (result as RestoredSession.ServerOnly)
assertEquals(server, current.server)
}
@Test
fun `Test restoreLastSession, nothing to restore`() =
runTest {
every { mockMostRecentServerProvider.get() } returns MostRecentServer.None
val result = create().restoreLastSession()
assertTrue(result is RestoredSession.None)
}
}
class TestMostRecentServerProvider : MostRecentServerProvider {
var mostRecentServer: MostRecentServer = MostRecentServer.None
override fun get(): MostRecentServer = mostRecentServer
override suspend fun save(current: CurrentUser) {
mostRecentServer =
if (current.user.accessToken.isNotNullOrBlank()) {
MostRecentServer.ServerAndUser(
current.server.id,
current.server.url,
current.user.id,
current.user.accessToken,
)
} else {
MostRecentServer.Server(
current.server.id,
current.server.url,
)
}
}
override suspend fun clear() {
mostRecentServer = MostRecentServer.None
}
override suspend fun clearUser() {
when (val s = mostRecentServer) {
MostRecentServer.None -> Unit
is MostRecentServer.Server -> Unit
is MostRecentServer.ServerAndUser -> MostRecentServer.Server(s.serverId, s.serverUrl)
}
}
}

View file

@ -5,8 +5,10 @@ import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import com.github.damontecres.wholphin.data.CurrentUser
import com.github.damontecres.wholphin.data.RestoredSession
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.test.currentUser
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import io.mockk.coEvery
import io.mockk.coVerify
@ -113,16 +115,16 @@ class SuggestionsWorkerTest {
val mockUser = mockk<CurrentUser>()
var restored = false
every { mockServerRepository.current } answers { MutableStateFlow(if (restored) mockUser else null) }
coEvery { mockServerRepository.restoreSession(testServerId, testUserId) } coAnswers {
coEvery { mockServerRepository.restoreLastSession() } coAnswers {
restored = true
mockUser
RestoredSession.Success(currentUser())
}
every { mockPreferences.data } returns flowOf(mockPrefs())
coEvery { mockUserViewsApi.getUserViews(userId = testUserId) } returns mockQueryResult()
val result = createWorker().doWork()
coVerify { mockServerRepository.restoreSession(testServerId, testUserId) }
coVerify { mockServerRepository.restoreLastSession() }
assertEquals(ListenableWorker.Result.success(), result)
}

View file

@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
import com.github.damontecres.wholphin.data.Migrations
import com.github.damontecres.wholphin.data.MostRecentServerProvider
import com.github.damontecres.wholphin.data.PlaybackEffectDao
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
import com.github.damontecres.wholphin.data.RememberedTabDao
@ -26,6 +27,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
import com.github.damontecres.wholphin.services.SeerrApi
import com.github.damontecres.wholphin.services.TestMostRecentServerProvider
import com.github.damontecres.wholphin.services.hilt.AppModule
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.services.hilt.DatabaseModule
@ -201,6 +203,10 @@ object TestModule {
fun seerrApi(
@StandardOkHttpClient okHttpClient: OkHttpClient,
): SeerrApi = mockk()
@Provides
@Singleton
fun mostRecentServerProvider(): MostRecentServerProvider = TestMostRecentServerProvider()
}
@Module