Route Wolfskin setup through the household profile picker
All checks were successful
review/code-claude Opus review PASS: no blockers, 10 non-blocking notes
review/safety-opus Opus review PASS: no blockers, 10 non-blocking notes

This commit is contained in:
justinzeus 2026-11-07 20:46:46 +01:00
parent 2b142af8d1
commit a5b5e92017
10 changed files with 983 additions and 2 deletions

View file

@ -0,0 +1,166 @@
// SPDX-License-Identifier: GPL-2.0-only
package org.justinvisser.wolfskin.tv
import android.app.Application
import android.content.Intent
import android.os.Looper
import android.view.View
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.Snapshot
import org.robolectric.Shadows
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.datastore.core.DataStore
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.github.damontecres.wholphin.MainActivityViewModel
import com.github.damontecres.wholphin.MainContent
import com.github.damontecres.wholphin.R
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.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.*
import com.github.takahirom.roborazzi.captureRoboImage
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.coVerify
import java.io.File
import java.io.IOException
import java.util.UUID
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/** Exercise the actual MainContent boundary, not a parallel copy of its destination mapping. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
class WolfskinSetupNavigationTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
private val server = JellyfinServer(UUID.randomUUID(), "Household", "https://jellyfin.test", null)
private val user = JellyfinUser(id = UUID.randomUUID(), serverId = server.id, name = "Justin", accessToken = "test-token")
private val repository = mockk<ServerRepository>(relaxed = true)
private val navigation = NavigationManager()
private val setup = mockk<SetupNavigationManager>(relaxed = true)
private val stack = mutableStateListOf<SetupDestination>(SetupDestination.Loading)
private val conductarr = ConductarrSession(MemoryCacheStore())
private val model = WolfskinSetupViewModel(repository, navigation, setup, WolfskinSession(), conductarr)
private val preferences = mockk<DataStore<AppPreferences>>()
private val intentService = mockk<IntentService>()
private var previousScreens = true
private var previousFixtures = false
@Before fun configure() {
WolfskinScreenPreference.initialize(rule.activity)
previousScreens = WolfskinScreenPreference.enabled
WolfskinScreenPreference.enabled = true
ConductarrPreference.initialize(rule.activity)
previousFixtures = ConductarrPreference.fixtures
ConductarrPreference.updateFixtures(false)
every { repository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { repository.getMostRecentServer() } returns RestoredSession.ServerOnly(server)
every { repository.serverDao.getServer(server.id) } returns JellyfinServerUsers(server, listOf(user))
every { repository.serverDao.getServers() } returns listOf(JellyfinServerUsers(server, listOf(user)))
every { setup.navigateTo(any()) } answers { stack[0] = firstArg() }
every { preferences.data } returns MutableStateFlow(AppPreferences.getDefaultInstance())
rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
@Suppress("DEPRECATION")
rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
@After fun close() {
conductarr.close()
WolfskinScreenPreference.enabled = previousScreens
ConductarrPreference.updateFixtures(previousFixtures)
}
private fun mount() {
val screensaver = mockk<ScreensaverService>(relaxed = true)
every { screensaver.state } returns MutableStateFlow(ScreensaverState(false, false, false, false))
rule.setContent {
MainContent(stack, navigation, UserPreferences(AppPreferences.getDefaultInstance(), null),
mockk(relaxed = true), screensaver, Modifier,
setupInterceptor = { destination, modifier -> WolfskinSetupContent(destination, modifier, model) })
}
}
private fun appModel() = MainActivityViewModel(rule.activity, preferences, repository, setup, navigation,
mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true), intentService)
private fun awaitStage(stage: WolfskinSetupViewModel.Stage) {
rule.waitUntil(5_000) {
Snapshot.sendApplyNotifications()
Shadows.shadowOf(Looper.getMainLooper()).idle()
model.stage == stage
}
}
private fun assertOwnPicker() {
awaitStage(WolfskinSetupViewModel.Stage.Picker)
rule.onNodeWithText("Who’s watching?").assertIsDisplayed()
rule.onNodeWithText("Justin").assertIsDisplayed()
rule.onNodeWithText(rule.activity.getString(R.string.select_server)).assertDoesNotExist()
rule.onNodeWithText(rule.activity.getString(R.string.select_user)).assertDoesNotExist()
}
@Test fun coldStartLegacySignOutDrawerSwitchAndIntentFailureNeverComposeStockSelectors() {
runBlocking { appModel().handleAppStart() }
mount()
assertOwnPicker()
// The legacy sign-out-equivalent erased the recent preference, but Room still has the household.
coEvery { repository.getMostRecentServer() } returns RestoredSession.None
rule.runOnIdle { setup.navigateTo(SetupDestination.ServerList) }
assertOwnPicker()
rule.runOnIdle { setup.navigateTo(SetupDestination.UserList(server)) }
assertOwnPicker()
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Cannot restore sign-in")
rule.runOnIdle { rule.activity.lifecycleScope.launch { appModel().handleIntent(Intent()) } }
rule.waitUntil(5_000) { stack.last() == SetupDestination.ServerList }
assertOwnPicker()
coVerify(exactly = 0) { repository.switchServerOrUser() }
}
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Test fun unreachableSelectionRendersWolfskinRecoveryFrame() {
stack[0] = SetupDestination.UserList(server)
coEvery { repository.changeUser(server, user) } throws IOException("offline")
mount()
assertOwnPicker()
rule.onNodeWithText("Justin").performClick()
awaitStage(WolfskinSetupViewModel.Stage.Unavailable)
rule.onNodeWithText("Try again").assertIsDisplayed()
rule.onNodeWithText("Sign in").assertIsDisplayed()
rule.onNodeWithText(rule.activity.getString(R.string.select_user)).assertDoesNotExist()
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")), "setup").apply { mkdirs() }
rule.onRoot().captureRoboImage(File(directory, "household-unreachable.png"))
}
@Test fun unconfiguredFirstRunOffersUrlInsteadOfStockServerSelector() {
coEvery { repository.getMostRecentServer() } returns RestoredSession.None
every { repository.serverDao.getServers() } returns emptyList()
stack[0] = SetupDestination.ServerList
mount()
awaitStage(WolfskinSetupViewModel.Stage.Unconfigured)
rule.onNodeWithText("Connect your household server").assertIsDisplayed()
rule.onNodeWithTag("household-server-url").assertIsDisplayed().performTextInput("https://jellyfin.test")
rule.onNodeWithText("Try again").assertIsEnabled()
rule.onNodeWithText(rule.activity.getString(R.string.select_server)).assertDoesNotExist()
}
}

View file

@ -0,0 +1,122 @@
// SPDX-License-Identifier: GPL-2.0-only
package org.justinvisser.wolfskin.tv
import android.app.Application
import android.content.pm.ActivityInfo
import android.os.Looper
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.core.app.ApplicationProvider
import com.github.damontecres.wholphin.MainContent
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.ScreensaverService
import com.github.damontecres.wholphin.services.ScreensaverState
import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.test.TestActivity
import com.github.damontecres.wholphin.ui.TestModule
import com.github.damontecres.wholphin.ui.successResponse
import com.github.damontecres.wholphin.util.WholphinDispatchers
import com.github.damontecres.wholphin.util.configure
import com.github.damontecres.wholphin.util.reset
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.api.operations.QuickConnectApi
import org.jellyfin.sdk.api.operations.SystemApi
import org.jellyfin.sdk.api.operations.UserApi
import org.jellyfin.sdk.model.api.PublicSystemInfo
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
/** The rollout switch must still expose both real upstream selectors, not a second forked copy. */
@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
@Config(application = HiltTestApplication::class, sdk = [34])
@RunWith(RobolectricTestRunner::class)
class WolfskinSetupRollbackTest {
@get:Rule(order = 0) val hilt = HiltAndroidRule(this)
@get:Rule(order = 1) val registerActivity = object : TestWatcher() {
override fun starting(description: Description?) {
val context = ApplicationProvider.getApplicationContext<Application>()
shadowOf(context.packageManager).addOrUpdateActivity(ActivityInfo().apply {
name = TestActivity::class.java.name
packageName = context.packageName
})
}
}
@get:Rule(order = 2) val rule = createAndroidComposeRule<TestActivity>()
@Inject lateinit var api: ApiClient
@Inject lateinit var jellyfin: Jellyfin
private var previousScreens = true
@Before fun setup() {
hilt.inject()
WholphinDispatchers.configure(TestModule.testDispatcher)
WolfskinScreenPreference.initialize(rule.activity)
previousScreens = WolfskinScreenPreference.enabled
WolfskinScreenPreference.enabled = false
val users = mockk<UserApi>()
val system = mockk<SystemApi>()
val quickConnect = mockk<QuickConnectApi>()
every { jellyfin.createApi(any(), any(), any(), any(), any()) } returns api
every { api.userApi } returns users
every { api.systemApi } returns system
every { api.quickConnectApi } returns quickConnect
coEvery { users.getPublicUsers() } returns successResponse(emptyList())
coEvery { system.getPublicSystemInfo() } returns successResponse(PublicSystemInfo(version = "10.11.11"))
coEvery { quickConnect.getQuickConnectEnabled() } returns successResponse(false)
}
@After fun restore() {
WolfskinScreenPreference.enabled = previousScreens
WholphinDispatchers.reset()
}
@Test fun rollbackRendersBothStockSelectors() {
val stack = mutableStateListOf<SetupDestination>(SetupDestination.ServerList)
val screensaver = mockk<ScreensaverService>()
every { screensaver.state } returns MutableStateFlow(ScreensaverState(false, false, false, false))
rule.setContent {
MainContent(stack, NavigationManager(), UserPreferences(AppPreferences.getDefaultInstance(), null),
mockk(relaxed = true), screensaver)
}
fun awaitHeading(resource: Int) {
rule.waitUntil(5_000) {
TestModule.testDispatcher.scheduler.advanceUntilIdle()
shadowOf(Looper.getMainLooper()).idle()
rule.onAllNodes(androidx.compose.ui.test.hasText(rule.activity.getString(resource))).fetchSemanticsNodes().isNotEmpty()
}
rule.onNodeWithText(rule.activity.getString(resource)).assertIsDisplayed()
rule.onNodeWithText("Who’s watching?").assertDoesNotExist()
}
awaitHeading(R.string.select_server)
rule.runOnIdle { stack[0] = SetupDestination.UserList(JellyfinServer(UUID.randomUUID(), "Household", "https://jellyfin.test", null)) }
awaitHeading(R.string.select_user)
}
}

View file

@ -0,0 +1,238 @@
// SPDX-License-Identifier: GPL-2.0-only
package org.justinvisser.wolfskin.tv
import android.app.Application
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.github.damontecres.wholphin.data.AppDatabase
import com.github.damontecres.wholphin.data.CurrentUser
import com.github.damontecres.wholphin.data.MostRecentServer
import com.github.damontecres.wholphin.data.MostRecentServerSharedPreferences
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
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.successResponse
import com.github.damontecres.wholphin.ui.TestModule
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import java.io.IOException
import java.util.UUID
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.test.runTest
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.api.operations.SystemApi
import org.jellyfin.sdk.api.operations.UserApi
import org.jellyfin.sdk.discovery.RecommendedServerInfo
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
import org.jellyfin.sdk.model.api.AuthenticationResult
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.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
import org.justinvisser.wolfskin.tv.WolfskinSetupViewModel.Stage
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/** Setup preserves saved credentials, rejects protected-profile bypasses and recovers real auth failures. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class)
class WolfskinSetupTest {
private val context get() = ApplicationProvider.getApplicationContext<Application>()
private val database by lazy { Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).allowMainThreadQueries().build() }
private val recent by lazy { MostRecentServerSharedPreferences(context) }
private val server = JellyfinServer(UUID.randomUUID(), "Household", "https://jellyfin.test", null)
private var user = JellyfinUser(id = UUID.randomUUID(), serverId = server.id, name = "Justin", accessToken = "saved-test-token")
private val api = mockk<ApiClient>(relaxed = true)
private val usersApi = mockk<UserApi>()
private val systemApi = mockk<SystemApi>()
private val jellyfin by lazy { TestModule.jellyfin(mockk(), context, ClientInfo("Wolfskin test", "test"), DeviceInfo("setup-test", "Setup test")) }
private val navigation = NavigationManager()
private val setup = mockk<SetupNavigationManager>(relaxed = true)
private val session = WolfskinSession()
private val conductarr = ConductarrSession(MemoryCacheStore())
private val repository by lazy { ServerRepository(context, jellyfin, database.serverDao(), api, Dispatchers.IO, recent) }
private val model by lazy { WolfskinSetupViewModel(repository, navigation, setup, session, conductarr) }
private val dto get() = UserDto(id = user.id, name = user.name, hasPassword = true, hasConfiguredPassword = true, hasConfiguredEasyPassword = false)
private var previousFixtures = false
@Before fun saveHousehold(): Unit = runBlocking {
ConductarrPreference.initialize(context)
previousFixtures = ConductarrPreference.fixtures
ConductarrPreference.updateFixtures(false)
recent.clear()
database.serverDao().addOrUpdateServer(server)
user = database.serverDao().addOrUpdateUser(user)
recent.save(CurrentUser(server, user))
val client = jellyfin
every { api.userApi } returns usersApi
every { api.systemApi } returns systemApi
every { client.createApi(any(), any(), any(), any(), any()) } returns api
coEvery { usersApi.getCurrentUser() } returns successResponse(dto)
coEvery { systemApi.getPublicSystemInfo() } returns successResponse(PublicSystemInfo(id = server.id.toString(), serverName = server.name))
}
@After fun close() {
conductarr.close()
database.close()
ConductarrPreference.updateFixtures(previousFixtures)
}
@Test fun enteringPickerPreservesServerAndUserAndRecoversErasedPreferenceFromRoom() = runBlocking {
val saved = recent.get()
model.load(null)
assertEquals(Stage.Picker, model.stage)
assertEquals(user.id.toString(), model.picker!!.person.jellyfinUserId)
assertEquals(saved, recent.get())
recent.clear()
model.load(null)
assertEquals(server, model.server)
assertEquals(Stage.Picker, model.stage)
assertEquals(MostRecentServer.None, recent.get())
}
@Test fun expiredTokenRequiresCredentialsAndSuccessfulLoginRestoresSessionWithoutSecondPicker() = runBlocking {
model.load(server)
coEvery { usersApi.getCurrentUser() } throws InvalidStatusException(401)
model.select(model.picker!!.person)
assertEquals(Stage.Login, model.stage)
assertEquals(user, model.loginUser)
assertNull(repository.current.value)
assertTrue(recent.get() is MostRecentServer.ServerAndUser)
coEvery { usersApi.authenticateUserByName(any()) } returns successResponse(AuthenticationResult(user = dto, serverId = server.id.toString(), accessToken = "renewed-test-token"))
coEvery { usersApi.getCurrentUser() } returns successResponse(dto)
model.login(user.name!!, "test-password")
assertEquals("renewed-test-token", repository.current.value!!.user.accessToken)
assertEquals(user.id, (recent.get() as MostRecentServer.ServerAndUser).userId)
assertFalse(session.state!!.pickerVisible)
assertEquals(user.id.toString(), session.state!!.person.jellyfinUserId)
}
@Test fun unreachableServerOffersRetryAndSavedPersonCanSignInWhenItReturns() = runTest {
model.load(server)
val saved = recent.get()
coEvery { usersApi.getCurrentUser() } throws IOException("offline")
model.select(model.picker!!.person)
assertEquals(Stage.Unavailable, model.stage)
assertEquals(saved, recent.get())
val retry = async(start = CoroutineStart.UNDISPATCHED) { model.retry() }
assertEquals(Stage.Loading, model.stage)
retry.await()
assertEquals(Stage.Picker, model.stage)
coEvery { usersApi.getCurrentUser() } returns successResponse(dto)
model.select(model.picker!!.person)
assertEquals(user.id, repository.current.value!!.user.id)
assertFalse(session.state!!.pickerVisible)
}
@Test fun retryCanRemainUnavailableWithoutErasingHousehold() = runTest {
database.serverDao().deleteUser(server.id, user.id)
val saved = recent.get()
coEvery { usersApi.getPublicUsers() } throws IOException("offline")
model.load(null)
assertEquals(Stage.Unavailable, model.stage)
val retry = async(start = CoroutineStart.UNDISPATCHED) { model.retry() }
assertEquals(Stage.Loading, model.stage)
retry.await()
assertEquals(Stage.Unavailable, model.stage)
assertEquals(server, model.server)
assertEquals(saved, recent.get())
}
@Test fun pinAndRequireLoginCannotReuseSavedTokenWithoutUnlocking() = runBlocking {
user = user.copy(pin = "URDL")
database.serverDao().updateUser(user)
model.load(server)
model.select(model.picker!!.person)
assertEquals(user, model.pinUser)
assertTrue(model.picker!!.pickerVisible)
model.enterPin("WRONG")
assertNull(repository.current.value)
model.enterPin("URDL")
assertEquals(user.id, repository.current.value!!.user.id)
user = user.copy(pin = null, requireLogin = true)
database.serverDao().updateUser(user)
model.load(server)
model.select(model.picker!!.person)
assertEquals(Stage.Login, model.stage)
assertEquals(user, model.loginUser)
}
@Test fun unavailablePublicProfilesKeepHouseholdAndAllowCredentialRecovery() = runBlocking {
database.serverDao().deleteUser(server.id, user.id)
coEvery { usersApi.getPublicUsers() } throws IOException("offline")
model.load(null)
assertEquals(Stage.Unavailable, model.stage)
assertEquals(server, model.server)
model.showLogin(null)
coEvery { usersApi.authenticateUserByName(any()) } returns successResponse(AuthenticationResult(
user = dto, serverId = server.id.toString(), accessToken = "new-test-token"))
model.login(user.name!!, "test-password")
assertEquals(user.id, repository.current.value!!.user.id)
}
@Test fun expiredLaunchRecoveryIgnoresOutagesLateIdentitiesAndRollbackMode() = runBlocking {
WolfskinScreenPreference.initialize(context)
val previous = WolfskinScreenPreference.enabled
try {
WolfskinScreenPreference.enabled = true
val expected = (repository.restoreSavedSession() as com.github.damontecres.wholphin.data.RestoredSession.Success).currentUser
var destination: SetupDestination = SetupDestination.AppContent(expected)
every { setup.navigateTo(any()) } answers { destination = firstArg() }
recoverWolfskinSession(context, IOException("offline"), expected, repository, setup)
assertTrue(destination is SetupDestination.AppContent)
WolfskinScreenPreference.enabled = false
recoverWolfskinSession(context, InvalidStatusException(401), expected, repository, setup)
assertTrue(destination is SetupDestination.AppContent)
WolfskinScreenPreference.enabled = true
val other = database.serverDao().addOrUpdateUser(user.copy(rowId = 0, id = UUID.randomUUID(), name = "Mindel"))
recent.save(CurrentUser(server, other))
repository.restoreSavedSession()
recoverWolfskinSession(context, InvalidStatusException(401), expected, repository, setup)
assertTrue(destination is SetupDestination.AppContent)
recoverWolfskinSession(context, InvalidStatusException(401), repository.current.value, repository, setup)
model.load((destination as SetupDestination.UserList).server)
assertEquals(Stage.Picker, model.stage)
assertTrue(model.picker!!.people.any { it.jellyfinUserId == other.id.toString() })
assertEquals(other.id, (recent.get() as MostRecentServer.ServerAndUser).userId)
} finally { WolfskinScreenPreference.enabled = previous }
}
@Test fun firstRunDiscoversAndPersistsHouseholdBeforeFetchingPublicProfiles() = runBlocking {
database.serverDao().deleteUser(server.id, user.id)
database.serverDao().deleteServer(server.id)
recent.clear()
model.load(null)
assertEquals(Stage.Unconfigured, model.stage)
coEvery { jellyfin.discovery.getRecommendedServers("bad-url") } returns emptyList()
model.connect("bad-url")
assertEquals(Stage.Unconfigured, model.stage)
assertTrue(database.serverDao().getServers().isEmpty())
coEvery { jellyfin.discovery.getRecommendedServers(server.url) } returns listOf(RecommendedServerInfo(
address = server.url, responseTime = 1, score = RecommendedServerInfoScore.GREAT, issues = emptyList(),
systemInfo = Result.success(PublicSystemInfo(id = server.id.toString(), serverName = server.name, startupWizardCompleted = true))))
model.connect(server.url)
assertEquals(server.id, database.serverDao().getServers().single().server.id)
coEvery { usersApi.getPublicUsers() } returns successResponse(listOf(dto))
model.load(model.server)
assertEquals(Stage.Picker, model.stage)
model.select(model.picker!!.person)
assertEquals(Stage.Login, model.stage)
assertNull(repository.current.value)
}
}