Keep Home responsive and resolve authorized request recipients
Some checks failed
review/code-claude Block: token sent to image hosts via Coil auth client; failed audiences read hides Jellyfin libraries

This commit is contained in:
justinzeus 2026-11-06 00:31:48 +01:00
parent 78322f588a
commit 09f1d72f30
13 changed files with 120 additions and 29 deletions

View file

@ -9,6 +9,9 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
@ -21,7 +24,6 @@ import java.io.File
import java.util.UUID
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Rule
@ -50,7 +52,8 @@ class ConductarrHomeSurfaceTest {
session.select("${server.id}:${user.id}", SHARED_TOKEN, fake.url)
runBlocking { session.authenticate() }
val home = mockk<HomeReadFacade>()
coEvery { home.read(user.id) } returns HomeLibrary()
coEvery { home.read(user.id) } returns HomeLibrary(resume = listOf(
HomeTitle("00000000-0000-0000-0000-000000000010", "Jellyfin held item", "movie", held = true)))
val cache = mockk<HomeLibraryCache>(relaxed = true)
every { cache.read(any()) } returns null
val model = WolfskinHomeViewModel(rule.activity, home, cache, NavigationManager(), WolfskinSeeAllLists(), session)
@ -59,15 +62,18 @@ class ConductarrHomeSurfaceTest {
rule.setContent { WolfskinTheme {
LaunchedEffect(model) { model.loadLibrary(server, user, "shared") }
val snapshot = model.snapshot
LaunchedEffect(snapshot.rows) {
val index = snapshot.rows.indexOfFirst { it.catalogue && it.cards.any { card -> card.title == "Moon" } }
if (index >= 0) { shell.view.row = index; shell.view.home.focusedKey = snapshot.rows[index].id; shell.navFocused = false }
}
CompositionLocalProvider(LocalScreenRatings provides ConductarrRatings(session.identity.value!!.client)) {
CompositionLocalProvider(LocalScreenRatings provides androidx.compose.runtime.remember(session.identity.value?.client) { ConductarrRatings(session.identity.value!!.client) }) {
ShellSurface(shell) { HomeScreen(shell, snapshot, it) { action -> model.act(action, shell, server, user) } }
}
} }
rule.waitUntil(10_000) { model.snapshot.rows.any { row -> row.cards.any { it.title == "Moon" } } }
rule.waitUntil(timeoutMillis = 30_000) {
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
model.snapshot.rows.any { row -> row.cards.any { it.title == "Moon" } }
}
rule.runOnIdle { shell.navFocused = false; shell.view.row = 0; shell.view.home.focusedKey = "hero" }
rule.onRoot().performKeyInput { pressKey(Key.DirectionDown) }
rule.mainClock.advanceTimeBy(500)
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
rule.onAllNodesWithText("Moon")[0].assertIsDisplayed()
assertTrue(model.snapshot.rows.filter { it.catalogue }.flatMap { it.cards }.none { it.held })
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory"))).apply { mkdirs() }

View file

@ -69,7 +69,7 @@ class ConductarrSessionTest {
fake.status = 200
fake.ratingsStatus = 401
val ratings = ConductarrRatings(reopened.identity.value!!.client).read(42, "movie", null, null)
assertNull(ratings)
assertTrue(ratings!!.isEmpty())
assertFalse(reopened.identity.value!!.signedIn)
assertNull(reopened.identity.value!!.operator)
assertEquals(ClientError.NeedsSignIn, reopened.identity.value!!.error)

View file

@ -45,11 +45,14 @@ class ConductarrSurfaceTest {
val identity by session.identity.collectAsState()
val state = rememberDiscoverState(shell, shell::enterDestination, { shell.enterDestination("discover") }, identity = identity, fixtures = false)
current = state
CompositionLocalProvider(LocalDiscoverArt provides { art }, LocalScreenRatings provides identity?.takeIf { it.signedIn }?.let { ConductarrRatings(it.client) }) {
CompositionLocalProvider(LocalDiscoverArt provides { art }, LocalScreenRatings provides identity?.takeIf { it.signedIn }?.let { selected -> androidx.compose.runtime.remember(selected.client) { ConductarrRatings(selected.client) } }) {
WolfskinTheme { ShellSurface(shell) { DiscoverScreen(state, it) } }
}
}
rule.waitUntil(10_000) { current.snapshot.status == DiscoverStatus.Ready }
rule.waitUntil(30_000) {
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
current.snapshot.status == DiscoverStatus.Ready
}
rule.onNodeWithText("Moon").assertIsDisplayed()
rule.mainClock.autoAdvance = false
rule.runOnIdle {

View file

@ -52,7 +52,7 @@ class ConductarrTitleSurfaceTest {
val viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk(), mockk<NavigationManager>(relaxed = true), session, mockk<ServerRepository>(relaxed = true))
try {
val route = titleRoute("movie:42")
val shell = ShellState(listOf(ShellPerson("shared", "Shared", "S", true), ShellPerson("mindel", "Mindel", "M", true)), "shared").apply {
val shell = ShellState(listOf(ShellPerson("shared", "Shared", "S", true)), "shared").apply {
pickerVisible = false; pickerLaunch = false; enterDestination(route)
}
val art = mutableMapOf<String, ImageBitmap>()
@ -67,7 +67,10 @@ class ConductarrTitleSurfaceTest {
rule.setContent { WolfskinTheme { CompositionLocalProvider(LocalTitleBitmaps provides art) {
TitleDestination(route, shell, UserPreferences(AppPreferences.getDefaultInstance(), null), viewModel = viewModel, fixtures = false)
} } }
rule.waitUntil(10_000) { viewModel.title?.data?.connection == TitleConnection.SignedIn }
rule.waitUntil(30_000) {
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
viewModel.title?.data?.connection == TitleConnection.SignedIn && session.identity.value!!.provider.requesterProfiles.value.any { it.key == "mindel" }
}
rule.onNodeWithText("Moon").assertIsDisplayed()
assertFalse(viewModel.title!!.options.any { it.id == "play" })
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory"))).apply { mkdirs() }
@ -75,7 +78,10 @@ class ConductarrTitleSurfaceTest {
rule.onRoot().performKeyInput { pressKey(Key.DirectionRight); pressKey(Key.DirectionCenter) }
rule.onNodeWithText("Request for Mindel").assertIsDisplayed()
rule.onRoot().performKeyInput { pressKey(Key.DirectionDown); pressKey(Key.DirectionCenter) }
rule.waitUntil(10_000) { viewModel.title?.data?.requestActor == "Shared" }
rule.waitUntil(30_000) {
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
viewModel.title?.data?.requestActor == "Shared"
}
assertEquals("Mindel", viewModel.title!!.data.requestFor)
rule.onAllNodesWithText("for Mindel · by Shared")[0].assertIsDisplayed()
val post = fake.calls.single { it.method == "POST" && it.url.encodedPath == "/api/v1/requests" }
@ -86,7 +92,10 @@ class ConductarrTitleSurfaceTest {
rule.onRoot().performKeyInput { pressKey(Key.DirectionCenter) }
fake.ratingsStatus = 401
runBlocking { ConductarrRatings(session.identity.value!!.client).read(45, "movie", null, null) }
rule.waitUntil(10_000) { viewModel.title?.data?.connection == TitleConnection.SignedOut && viewModel.title?.data?.status == TitleReadStatus.Unavailable }
rule.waitUntil(30_000) {
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
viewModel.title?.data?.connection == TitleConnection.SignedOut && viewModel.title?.data?.status == TitleReadStatus.Unavailable
}
assertTrue(viewModel.title!!.data.item.ratings.isEmpty())
assertFalse(viewModel.title!!.options.any { it.id in listOf("request", "other", "add") })
} finally { viewModel.viewModelScope.cancel() }

View file

@ -75,7 +75,8 @@ internal class FakeConductarr : Closeable {
if (key == "season" && path.endsWith("/2")) body = JsonObject(body + ("episodes" to JsonArray(body.getValue("episodes").jsonArray.map {
JsonObject(it.jsonObject + ("vote_average" to JsonPrimitive(seasonTwoScore)))
})))
return response(if (localArt) localizeImages(body).toString() else body.toString())
val responseBody = if (localArt) localizeImages(body).toString() else body.toString()
return response(responseBody)
}
}
server.start()