Android: public host as the build property kvitt.publicHost (D-121)

The release server's host name comes from -Pkvitt.publicHost, else kvitt.publicHost in
android/local.properties, else the placeholder kvitt.example.invalid. :app exposes it as
BuildConfig.PUBLIC_HOST, the manifest placeholder publicHost (the https invite-link intent
filter, port 8443) and RELEASE_SERVER_URL, which :feature:onboarding receives through the
@ReleaseServerUrl Hilt qualifier for S-1's prefill. Tests and goldens use fixed hosts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-19 19:58:58 +02:00
parent 9231a1d755
commit d090c6c850
15 changed files with 156 additions and 50 deletions
@@ -11,6 +11,7 @@ import se.hagstedt.kvitt.fakes.FakeRepositories
import se.hagstedt.kvitt.fakes.FakeScenario
import se.hagstedt.kvitt.fakes.FakeScenarios
import se.hagstedt.kvitt.link.AppLink
import se.hagstedt.kvitt.link.RELEASE_SERVER_URL
import se.hagstedt.kvitt.model.SessionState
import se.hagstedt.kvitt.design.testing.MainDispatcherRule
@@ -22,7 +23,7 @@ class AppViewModelTest {
private fun vm(fakes: FakeRepositories) = AppViewModel(fakes.session, fakes.sync, fakes.accounts)
private val link = AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCD2345EFGH", "Jacob")
private val link = AppLink.Invite(RELEASE_SERVER_URL, "ABCD2345EFGH", "Jacob")
@Test
fun anInviteLinkWhilePaired_isDropped_andNeverPrefillsALaterPairing() {
@@ -4,17 +4,43 @@ import android.app.Application
import android.content.Intent
import android.net.Uri
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import se.hagstedt.kvitt.BuildConfig
import se.hagstedt.kvitt.feature.onboarding.releaseServerUrl
/** AC-2.6 invite links and AC-13.6 notification taps as `MainActivity` receives them. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [36], application = Application::class)
class AppLinksTest {
/** The host this build was configured with (`kvitt.publicHost`, D-121). */
private val host = BuildConfig.PUBLIC_HOST
@Test
fun theReleaseServerIsHttpsOnPort8443OfThePublicHost() {
assertEquals("https://$host:8443", RELEASE_SERVER_URL)
// Without kvitt.publicHost the build uses the placeholder, which never resolves (RFC 2606).
assertEquals("https://kvitt.example.invalid:8443", releaseServerUrl("kvitt.example.invalid"))
}
@Test
fun theManifestOpensLandingLinksOfThePublicHostOnly() {
val context = RuntimeEnvironment.getApplication()
fun opens(url: String) = context.packageManager.queryIntentActivities(
Intent(Intent.ACTION_VIEW, Uri.parse(url)).addCategory(Intent.CATEGORY_BROWSABLE).setPackage(context.packageName),
0,
).any { it.activityInfo.name == "se.hagstedt.kvitt.MainActivity" }
assertTrue(opens("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN"))
assertFalse(opens("https://other.example.org:8443/i/ABCDEFGHJKMN"))
assertFalse(opens("https://$host/i/ABCDEFGHJKMN"))
}
@Test
fun appLinkFillsServerAndCode() {
val link = AppLinks.parseUri("kvitt://invite?server=http%3A%2F%2F192.168.1.210%3A8080&code=ABCD-EFGH-JKMN")
@@ -24,33 +50,33 @@ class AppLinksTest {
@Test
fun appLinkNormalisesTheCode() {
// §8.3: lowercase, O -> 0, I and L -> 1.
assertEquals("ABCD0113EFGH", AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2Ficehack.asuscomm.com%3A8443&code=abcdo1l3efgh")?.code)
assertEquals("ABCD0113EFGH", AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2F$host%3A8443&code=abcdo1l3efgh")?.code)
}
@Test
fun landingLinkFillsTheServerFromTheLink() {
val link = AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/ABCDEFGHJKMN")
assertEquals(AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN"), link)
val link = AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")
assertEquals(AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN"), link)
}
@Test
fun linksWithTheAdminNameCarryIt_andOldLinksStillParse() {
assertEquals(
AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN", "Åsa Berg"),
AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/ABCDEFGHJKMN?from=%C3%85sa%20Berg"),
AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN", "Åsa Berg"),
AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN?from=%C3%85sa%20Berg"),
)
assertEquals(
AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN", "Jacob"),
AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2Ficehack%2Easuscomm%2Ecom%3A8443&code=ABCDEFGHJKMN&from=Jacob"),
AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN", "Jacob"),
AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2F${host.replace(".", "%2E")}%3A8443&code=ABCDEFGHJKMN&from=Jacob"),
)
assertNull(AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/ABCDEFGHJKMN")?.adminName)
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")?.adminName)
}
@Test
fun otherLinksAreIgnored() {
assertNull(AppLinks.parseUri(null))
assertNull(AppLinks.parseUri("https://icehack.asuscomm.com:8443/apk/latest"))
assertNull(AppLinks.parseUri("https://icehack.asuscomm.com:8443/i/SHORT"))
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/apk/latest"))
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/i/SHORT"))
assertNull(AppLinks.parseUri("kvitt://invite?server=x"))
assertNull(AppLinks.parseUri("kvitt://other?code=ABCDEFGHJKMN"))
// A bare code is a paste on S-1, not a link.
@@ -59,8 +85,8 @@ class AppLinksTest {
@Test
fun viewIntentIsParsed() {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://icehack.asuscomm.com:8443/i/ABCD-EFGH-JKMN"))
assertEquals(AppLink.Invite("https://icehack.asuscomm.com:8443", "ABCDEFGHJKMN"), AppLinks.parse(intent))
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("$RELEASE_SERVER_URL/i/ABCD-EFGH-JKMN"))
assertEquals(AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN"), AppLinks.parse(intent))
}
@Test