Server-controlled app updates, optional or forced
The gateway decides whether a TV may keep running its current build. Clients send X-Memby-Version on every request and ask GET /v1/update on each launch; the verdict is none, optional or mandatory. - internal/appupdate holds the decision as pure, tested logic: below minimumVersion is mandatory, below latestVersion is optional. - Admin page gains an App updates section — latest version, APK URL, notes, and a "Require this update" toggle that sets the forced floor. - Client shows a dismissable prompt for optional, and a full-screen panel that swallows Back for mandatory. Instructions say what the system installer will ask before it asks. Two safeguards: a client that cannot report its version is never forced, and the client ignores a verdict with no download URL, so a half-configured policy cannot produce an unblockable screen with a dead button. An unreachable gateway shows nothing. The verdict is deliberately not part of /v1/home: that payload is cached per user, while this answer depends on the requesting client's version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
08360b75e4
commit
62f6345a40
@@ -0,0 +1,77 @@
|
||||
package com.ponzischeme89.memby.data
|
||||
|
||||
import com.ponzischeme89.memby.data.model.GatewayUpdate
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The update verdict decides whether a viewer can use the app at all, so the client's
|
||||
* reading of it needs pinning down — including the cases that must NOT block anyone.
|
||||
*/
|
||||
class GatewayUpdateTest {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true; coerceInputValues = true }
|
||||
|
||||
@Test
|
||||
fun `a mandatory verdict is actionable and blocking`() {
|
||||
val update = json.decodeFromString<GatewayUpdate>(
|
||||
"""{"status":"mandatory","version":"0.1.54","notes":"Security fix","downloadUrl":"https://nas/memby.apk"}""",
|
||||
)
|
||||
|
||||
assertTrue(update.isMandatory)
|
||||
assertFalse(update.isOptional)
|
||||
assertTrue(update.isActionable)
|
||||
assertEquals("0.1.54", update.version)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an optional verdict is dismissable`() {
|
||||
val update = json.decodeFromString<GatewayUpdate>(
|
||||
"""{"status":"optional","version":"0.1.54","downloadUrl":"https://nas/memby.apk"}""",
|
||||
)
|
||||
|
||||
assertTrue(update.isOptional)
|
||||
assertFalse(update.isMandatory)
|
||||
assertTrue(update.isActionable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `status none is never shown`() {
|
||||
val update = json.decodeFromString<GatewayUpdate>("""{"status":"none"}""")
|
||||
|
||||
assertFalse(update.isActionable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a verdict with no download url is not actionable`() {
|
||||
// Nothing to act on: showing an unblockable prompt with no working button would
|
||||
// strand the viewer completely.
|
||||
val update = json.decodeFromString<GatewayUpdate>(
|
||||
"""{"status":"mandatory","version":"0.1.54","downloadUrl":""}""",
|
||||
)
|
||||
|
||||
assertTrue(update.isMandatory)
|
||||
assertFalse(update.isActionable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an empty response decodes to nothing to do`() {
|
||||
val update = json.decodeFromString<GatewayUpdate>("{}")
|
||||
|
||||
assertEquals(GatewayUpdate.STATUS_NONE, update.status)
|
||||
assertFalse(update.isActionable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unrecognised status from a newer gateway is treated as nothing`() {
|
||||
val update = json.decodeFromString<GatewayUpdate>(
|
||||
"""{"status":"nag","version":"0.1.54","downloadUrl":"https://nas/memby.apk"}""",
|
||||
)
|
||||
|
||||
assertFalse(update.isMandatory)
|
||||
assertFalse(update.isOptional)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user