Self-hosted APK distribution
Memby is handed out from a NAS or web server rather than a store, so:
- UpdateChecker gains a static-manifest source alongside Gitea. A .json
update URL is read as a manifest ({version, apkUrl, notes}); anything
else is still treated as a Gitea host. apkUrl may be relative and is
resolved against the manifest's own URL.
- Release signing config reads memby.keystore from local.properties or
the environment. Without it the build still succeeds but warns loudly:
an unsigned APK will not install, and a changed key forces every user
to uninstall before they can update.
- release.ps1 bumps the version, runs tests, builds a signed APK and
assembles dist/out/ (landing page, latest.json, versioned APK) ready to
copy onto the NAS.
- Keystores and dist/out are gitignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2ce405c540
commit
08360b75e4
@@ -0,0 +1,76 @@
|
||||
package com.ponzischeme89.memby.update
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The update source is chosen by URL shape, and a manifest's apkUrl may be relative.
|
||||
* Both are easy to get subtly wrong and would strand every TV on an old build.
|
||||
*/
|
||||
class UpdateSourceTest {
|
||||
|
||||
@Test
|
||||
fun `a json url is a static manifest`() {
|
||||
assertTrue(isManifestUrl("https://nas.example.com/memby/latest.json"))
|
||||
assertTrue(isManifestUrl("https://nas.example.com/memby/latest.JSON"))
|
||||
assertTrue(isManifestUrl("https://nas.example.com/memby/latest.json?v=2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a bare host is treated as gitea`() {
|
||||
assertFalse(isManifestUrl("https://g.sublogue.com"))
|
||||
assertFalse(isManifestUrl("https://g.sublogue.com/"))
|
||||
// A folder that merely contains json elsewhere is not a manifest.
|
||||
assertFalse(isManifestUrl("https://nas.example.com/json"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an absolute apk url is used as-is`() {
|
||||
val resolved = resolveApkUrl(
|
||||
"https://nas.example.com/memby/latest.json",
|
||||
"https://cdn.example.com/memby-0.1.54.apk",
|
||||
)
|
||||
assertEquals("https://cdn.example.com/memby-0.1.54.apk", resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a relative apk url resolves next to the manifest`() {
|
||||
val resolved = resolveApkUrl(
|
||||
"https://nas.example.com/memby/latest.json",
|
||||
"memby-0.1.54.apk",
|
||||
)
|
||||
assertEquals("https://nas.example.com/memby/memby-0.1.54.apk", resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a root-relative apk url keeps the host`() {
|
||||
val resolved = resolveApkUrl(
|
||||
"https://nas.example.com/memby/latest.json",
|
||||
"/downloads/memby-0.1.54.apk",
|
||||
)
|
||||
assertEquals("https://nas.example.com/downloads/memby-0.1.54.apk", resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a query string on the manifest does not leak into the apk url`() {
|
||||
val resolved = resolveApkUrl(
|
||||
"https://nas.example.com/memby/latest.json?nocache=1",
|
||||
"memby-0.1.54.apk",
|
||||
)
|
||||
assertEquals("https://nas.example.com/memby/memby-0.1.54.apk", resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `manifest decodes with the field names the release script writes`() {
|
||||
val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }
|
||||
val manifest = json.decodeFromString<UpdateManifest>(
|
||||
"""{"version":"0.1.54","apkUrl":"memby-0.1.54.apk","notes":"Faster home screen"}""",
|
||||
)
|
||||
|
||||
assertEquals("0.1.54", manifest.version)
|
||||
assertEquals("memby-0.1.54.apk", manifest.apkUrl)
|
||||
assertEquals("Faster home screen", manifest.notes)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user