Files
memby/app/src/test/java/com/ponzischeme89/memby/data/GatewayPayloadTest.kt
T
2026-07-27 21:06:51 +12:00

158 lines
5.9 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.ponzischeme89.memby.data
import com.ponzischeme89.memby.data.model.GatewayHome
import com.ponzischeme89.memby.data.model.GatewayPlayback
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Contract tests for the gateway wire format.
*
* The fixtures below are the exact shape `server/internal/api` emits: camelCase envelope
* fields wrapping Emby's own PascalCase item JSON. If someone renames a field on either
* side, this fails before a TV ever sees it.
*/
class GatewayPayloadTest {
private val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
isLenient = true
explicitNulls = false
}
@Test
fun `decodes a home payload with emby-shaped items`() {
val payload = """
{
"continueWatching": [
{"Id":"1","Name":"Dune","Type":"Movie","UserData":{"PlaybackPositionTicks":36000000000}}
],
"nextUp": [
{"Id":"2","Name":"Pilot","Type":"Episode","SeriesName":"Severance"}
],
"favorites": [
{"Id":"3","Name":"Arrival","Type":"Movie","UserData":{"IsFavorite":true}}
],
"latestMovies": [],
"partial": false
}
""".trimIndent()
val home = json.decodeFromString<GatewayHome>(payload)
assertEquals("Dune", home.continueWatching.single().name)
assertEquals(3_600_000L, home.continueWatching.single().userData!!.playbackPositionTicks / 10_000L)
assertEquals("Severance", home.nextUp.single().seriesName)
assertTrue(home.favorites.single().isFavorite)
assertTrue(home.latestMovies.isEmpty())
assertEquals(false, home.partial)
}
@Test
fun `decodes server-composed rows including recommendations`() {
val payload = """
{
"rows": [
{"id":"continue","title":"Continue Watching","kind":"continue","items":[{"Id":"1","Name":"Dune","Type":"Movie"}]},
{"id":"favorites","title":"Favourites","kind":"favorites","items":[{"Id":"3","Name":"Arrival","Type":"Movie"}]},
{"id":"similar:sev","title":"Because you watched Severance","kind":"similar","items":[{"Id":"7","Name":"Devs","Type":"Series"}]},
{"id":"recommended","title":"Recommended from your watching history","kind":"recommended","items":[{"Id":"8","Name":"Solaris","Type":"Movie"}]}
],
"continueWatching": [{"Id":"1","Name":"Dune","Type":"Movie"}],
"nextUp": [],
"favorites": [{"Id":"3","Name":"Arrival","Type":"Movie"}],
"latestMovies": [],
"partial": false
}
""".trimIndent()
val home = json.decodeFromString<GatewayHome>(payload)
assertEquals(
listOf("continue", "favorites", "similar:sev", "recommended"),
home.rows.map { it.id },
)
assertEquals("Because you watched Severance", home.rows[2].title)
assertEquals("Solaris", home.rows.last().items.single().name)
}
@Test
fun `decodes the informational Sonarr schedule row`() {
val payload = """
{
"rows": [{
"id":"sonarr-airing-today",
"title":"Shows airing today",
"kind":"schedule",
"items":[{
"Id":"sonarr:7:42",
"Name":"Northbound",
"Type":"MembySonarrEpisode",
"ImageTags":{"Primary":"sonarr"},
"MembySource":"sonarr",
"MembyEpisodeTitle":"The Crossing",
"MembyEpisodeCode":"S02E04",
"MembyAirLabel":"Airs today at 8:00 PM",
"MembyAvailability":"downloading",
"MembyAvailabilityText":"Downloading",
"MembyPlayable":false
}]
}],
"continueWatching":[],
"nextUp":[],
"favorites":[],
"latestMovies":[],
"partial":false
}
""".trimIndent()
val item = json.decodeFromString<GatewayHome>(payload).rows.single().items.single()
assertTrue(item.isSonarrSchedule)
assertEquals("S02E04", item.membyEpisodeCode)
assertEquals("Downloading", item.membyAvailabilityText)
assertEquals(false, item.membyPlayable)
}
@Test
fun `a home payload without rows still decodes`() {
// The gateway omits recommendation rows while they are still building, and an
// older gateway would not send `rows` at all.
val home = json.decodeFromString<GatewayHome>(
"""{"continueWatching":[],"nextUp":[],"favorites":[],"latestMovies":[],"partial":false}""",
)
assertTrue(home.rows.isEmpty())
}
@Test
fun `a partial home payload still decodes`() {
val home = json.decodeFromString<GatewayHome>(
"""{"continueWatching":[],"nextUp":[],"favorites":[],"latestMovies":[],"partial":true}""",
)
assertTrue(home.partial)
}
@Test
fun `decodes a playback response`() {
val playback = json.decodeFromString<GatewayPlayback>(
"""{"itemId":"9","title":"Severance Pilot","url":"https://emby.example/Videos/9/stream?static=true","resumePositionMs":42000}""",
)
assertEquals("9", playback.itemId)
assertEquals(42_000L, playback.resumePositionMs)
assertTrue(playback.url.startsWith("https://emby.example/Videos/9/stream"))
}
@Test
fun `device allowance response becomes a specific sign-in error`() {
val error = parseDeviceLimit(
"""{"error":"device_limit_reached","activeClients":3,"maxClientsPerUser":3}""",
)
assertEquals(3, error?.activeClients)
assertEquals(3, error?.maxClients)
}
}