Android: old host name out of the pinned-root config and the S-1 fixture

- Pinned-root network security config (D-082) is now a template in
  app/src/releasePinnedRoot/; with kvitt.tls.pinnedRoot=true the task
  generatePinnedRootNetworkSecurityConfig writes it into the release
  resources with the domain kvitt.publicHost (D-121), and stops with a
  clear message while the root src/release/res/raw/kvitt_root_ca.crt is
  not committed (fallback unprepared, SPEC 10.5.3). Comment: pinned NPM
  root (D-120).
- Port 443 (D-124): RELEASE_SERVER_URL is https://<kvitt.publicHost>
  without a port (RELEASE_SERVER_PORT removed); the AC-2.6 intent filter
  has no android:port; InviteLink drops an explicit default port.
- S-1 fixture GOLDEN_SERVER https://kvitt.example.org; 28 S-1 goldens
  re-recorded (only the server row changed).

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 20:27:29 +02:00
parent 4852803cdb
commit 038c6036fe
40 changed files with 97 additions and 44 deletions
@@ -45,8 +45,10 @@ object InviteLink {
val segments = path.trim('/').split('/')
if (segments.size != 2 || segments[0] != "i") return null
val code = code(segments[1]) ?: return null
val port = if (uri.port == -1) "" else ":${uri.port}"
return PastedInvite(code, "${uri.scheme.lowercase()}://${uri.host}$port", adminName(uri.rawQuery?.let(::params)?.get(FROM)))
val scheme = uri.scheme.lowercase()
// An explicit default port is dropped, so `https://<host>:443/i/…` names the release server `https://<host>`.
val port = if (uri.port == -1 || uri.port == DEFAULT_PORTS[scheme]) "" else ":${uri.port}"
return PastedInvite(code, "$scheme://${uri.host}$port", adminName(uri.rawQuery?.let(::params)?.get(FROM)))
}
/** The first value of each key; a value that is not valid percent-encoding is dropped. */
@@ -67,6 +69,7 @@ object InviteLink {
}
private const val FROM = "from"
private val DEFAULT_PORTS = mapOf("https" to 443, "http" to 80)
/** The normalised code when [text] normalises to exactly 12 alphabet characters (§8.3), else null. */
private fun code(text: String): String? {
@@ -4,14 +4,12 @@ import java.net.URI
import java.net.URISyntaxException
import javax.inject.Qualifier
/** The release server's port: the router forwards external 8443 to the TLS proxy (SPEC §10.1, D-120). */
const val RELEASE_SERVER_PORT = 8443
/**
* The release server of a public host name, `https://<host>:8443` (AC-2.7, D-121). `:app` builds it from its
* `BuildConfig.PUBLIC_HOST` (the Gradle property `kvitt.publicHost`) and provides it as [ReleaseServerUrl].
* The release server of a public host name, `https://<host>` on the default port 443, where NPM selects Kvitt by host
* name (AC-2.7, D-121, D-124). `:app` builds it from its `BuildConfig.PUBLIC_HOST` (the Gradle property
* `kvitt.publicHost`) and provides it as [ReleaseServerUrl].
*/
fun releaseServerUrl(publicHost: String): String = "https://$publicHost:$RELEASE_SERVER_PORT"
fun releaseServerUrl(publicHost: String): String = "https://$publicHost"
/** The release server prefilled on S-1 (AC-2.7), `RELEASE_SERVER_URL` in `:app` (D-121). */
@Qualifier
@@ -24,7 +24,7 @@ class InviteLinkTest {
@Test
fun theAppLinkGivesCodeAndServer() {
val parsed = InviteLink.parse("kvitt://invite?server=https%3A%2F%2Fkvitt.example.org%3A8443&code=7K3MQX9D2HPA")
val parsed = InviteLink.parse("kvitt://invite?server=https%3A%2F%2Fkvitt.example.org&code=7K3MQX9D2HPA")
assertEquals(PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL), parsed)
assertNull(InviteLink.parse("kvitt://other?code=7K3MQX9D2HPA"))
}
@@ -33,21 +33,28 @@ class InviteLinkTest {
fun linksMayNameTheAdmin_andABadNameIsDropped() {
assertEquals(
PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL, "Jacob"),
InviteLink.parse("kvitt://invite?server=https%3A%2F%2Fkvitt.example.org%3A8443&code=7K3MQX9D2HPA&from=Jacob"),
InviteLink.parse("kvitt://invite?server=https%3A%2F%2Fkvitt.example.org&code=7K3MQX9D2HPA&from=Jacob"),
)
assertEquals(
PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL, "Åsa Berg"),
InviteLink.parse("https://kvitt.example.org:8443/i/7K3MQX9D2HPA?from=%C3%85sa+Berg"),
InviteLink.parse("https://kvitt.example.org/i/7K3MQX9D2HPA?from=%C3%85sa+Berg"),
)
for (bad in listOf("", "%20", "a%0Ab", "A".repeat(41))) {
assertEquals(bad, PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL), InviteLink.parse("https://kvitt.example.org:8443/i/7K3MQX9D2HPA?from=$bad"))
assertEquals(bad, PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL), InviteLink.parse("https://kvitt.example.org/i/7K3MQX9D2HPA?from=$bad"))
}
}
@Test
fun theLandingLinkGivesCodeAndServer() {
assertEquals(PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL), InviteLink.parse("https://kvitt.example.org:8443/i/7K3MQX9D2HPA"))
assertEquals(PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL), InviteLink.parse("https://kvitt.example.org/i/7K3MQX9D2HPA"))
assertEquals(PastedInvite("7K3MQX9D2HPA", "http://192.168.1.210:8080"), InviteLink.parse("http://192.168.1.210:8080/i/7k3mqx9d2hpa"))
assertNull(InviteLink.parse("https://kvitt.example.org:8443/x/7K3MQX9D2HPA"))
assertNull(InviteLink.parse("https://kvitt.example.org/x/7K3MQX9D2HPA"))
}
@Test
fun anExplicitDefaultPortIsDropped_andAnyOtherPortKept() {
assertEquals(PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL), InviteLink.parse("https://kvitt.example.org:443/i/7K3MQX9D2HPA"))
assertEquals(PastedInvite("7K3MQX9D2HPA", "http://192.168.1.210"), InviteLink.parse("http://192.168.1.210:80/i/7K3MQX9D2HPA"))
assertEquals(PastedInvite("7K3MQX9D2HPA", "https://kvitt.example.org:8443"), InviteLink.parse("https://kvitt.example.org:8443/i/7K3MQX9D2HPA"))
}
}
@@ -169,7 +169,7 @@ class PairingViewModelTest {
@Test
fun pasteFillsCodeAndServerOrSaysTheCodeHasTwelveCharacters() {
val (_, vm) = vm()
vm.onPaste("https://kvitt.example.org:8443/i/7K3MQX9D2HPA")
vm.onPaste("https://kvitt.example.org/i/7K3MQX9D2HPA")
assertEquals("7K3MQX9D2HPA", vm.state.value.code)
assertNull(vm.state.value.error)
vm.onPaste("hello")
@@ -42,10 +42,11 @@ class ServerAddressTest {
}
@Test
fun theReleaseServerIsHttpsOnPort8443OfThePublicHost() {
// D-121: the build's default kvitt.publicHost is the placeholder, which never resolves (RFC 2606).
assertEquals("https://kvitt.example.invalid:8443", releaseServerUrl("kvitt.example.invalid"))
assertEquals("https://kvitt.duckdns.org:8443", releaseServerUrl("kvitt.duckdns.org"))
fun theReleaseServerIsHttpsOnTheDefaultPortOfThePublicHost() {
// D-121: the build's default kvitt.publicHost is the placeholder, which never resolves (RFC 2606); D-124: port 443,
// written without a port.
assertEquals("https://kvitt.example.invalid", releaseServerUrl("kvitt.example.invalid"))
assertEquals("https://kvitt.duckdns.org", releaseServerUrl("kvitt.duckdns.org"))
}
@Test
@@ -27,9 +27,10 @@ private const val LAN_SERVER = "http://192.168.1.210:8080"
/**
* The release server the goldens show on S-1's server row. Fixed, not `:app`'s `BuildConfig.PUBLIC_HOST`, so a
* checkout's `kvitt.publicHost` never changes a render (D-121); the host is the one the goldens were recorded with.
* checkout's `kvitt.publicHost` never changes a render (D-121); the fixture host of the other modules, on port 443
* (D-124).
*/
private const val GOLDEN_SERVER = "https://icehack.asuscomm.com:8443"
private const val GOLDEN_SERVER = "https://kvitt.example.org"
/**
* Every state of S-1, S-2, S-3 and S-18 (design-spec §4 and PLAN-M7a §5). Each is produced by the screen's real
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB