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
@@ -18,7 +18,7 @@ val localProperties = Properties().apply {
|
|||||||
val releaseStoreFile: String? = localProperties.getProperty("kvitt.release.storeFile")
|
val releaseStoreFile: String? = localProperties.getProperty("kvitt.release.storeFile")
|
||||||
|
|
||||||
// D-121: Kvitt's public host name (SPEC §8.5, §10.6), as BuildConfig.PUBLIC_HOST, the manifest placeholder publicHost
|
// D-121: Kvitt's public host name (SPEC §8.5, §10.6), as BuildConfig.PUBLIC_HOST, the manifest placeholder publicHost
|
||||||
// (the https invite links of AC-2.6, port 8443) and RELEASE_SERVER_URL = https://<host>:8443 (AC-2.7). From
|
// (the https invite links of AC-2.6, default port 443, D-124) and RELEASE_SERVER_URL = https://<host> (AC-2.7). From
|
||||||
// -Pkvitt.publicHost (infra/publish-apk.sh passes it, from KVITT_PUBLIC_HOST in infra/pi/.env), else the key
|
// -Pkvitt.publicHost (infra/publish-apk.sh passes it, from KVITT_PUBLIC_HOST in infra/pi/.env), else the key
|
||||||
// kvitt.publicHost in android/local.properties (the owner sets it there; never committed), else the placeholder
|
// kvitt.publicHost in android/local.properties (the owner sets it there; never committed), else the placeholder
|
||||||
// kvitt.example.invalid (RFC 2606: never resolves), so a release built without it cannot talk to a real host. Debug
|
// kvitt.example.invalid (RFC 2606: never resolves), so a release built without it cannot talk to a real host. Debug
|
||||||
@@ -32,9 +32,31 @@ require(Regex("[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+"
|
|||||||
"kvitt.publicHost must be a host name such as kvitt.duckdns.org (no scheme, port or path), was '$publicHost'"
|
"kvitt.publicHost must be a host name such as kvitt.duckdns.org (no scheme, port or path), was '$publicHost'"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPEC §10.5.3: the pinned Caddy root replaces the system anchors for icehack.asuscomm.com only when this is true.
|
// SPEC §10.5.3 (D-082): the pinned NPM root (D-120) replaces the system anchors for kvitt.publicHost only when this is true.
|
||||||
val pinnedRoot = providers.gradleProperty("kvitt.tls.pinnedRoot").orElse("false").get().toBoolean()
|
val pinnedRoot = providers.gradleProperty("kvitt.tls.pinnedRoot").orElse("false").get().toBoolean()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes res/xml/network_security_config.xml from [template] with `@PUBLIC_HOST@` replaced by [host] (D-121). The
|
||||||
|
* fallback is not prepared (§10.5.3): without the committed root [rootCa] the build stops here with that message.
|
||||||
|
*/
|
||||||
|
abstract class PinnedRootConfigTask : DefaultTask() {
|
||||||
|
@get:InputFile @get:PathSensitive(PathSensitivity.NONE) abstract val template: RegularFileProperty
|
||||||
|
@get:Input abstract val host: Property<String>
|
||||||
|
@get:Internal abstract val rootCa: RegularFileProperty
|
||||||
|
@get:OutputDirectory abstract val outputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun write() {
|
||||||
|
val ca = rootCa.get().asFile
|
||||||
|
if (!ca.isFile) throw GradleException("kvitt.tls.pinnedRoot=true needs the pinned root ${ca.path} (SPEC §10.5.3)")
|
||||||
|
val root = outputDir.get().asFile
|
||||||
|
root.deleteRecursively()
|
||||||
|
val xml = root.resolve("xml/network_security_config.xml")
|
||||||
|
xml.parentFile.mkdirs()
|
||||||
|
xml.writeText(template.get().asFile.readText().replace("@PUBLIC_HOST@", host.get()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "se.hagstedt.kvitt"
|
namespace = "se.hagstedt.kvitt"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -77,10 +99,20 @@ android {
|
|||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sourceSets {
|
}
|
||||||
// The pinned-root network security config (and the committed kvitt_root_ca.crt in src/release/res/raw) only
|
|
||||||
// when kvitt.tls.pinnedRoot=true; otherwise src/main's system-anchors config applies.
|
// The pinned-root network security config (generated from src/releasePinnedRoot, trusting src/release/res/raw/
|
||||||
if (pinnedRoot) getByName("release").res.directories.add("src/releasePinnedRoot/res")
|
// kvitt_root_ca.crt) only in release builds and only when kvitt.tls.pinnedRoot=true; otherwise src/main's
|
||||||
|
// system-anchors config applies.
|
||||||
|
if (pinnedRoot) {
|
||||||
|
val pinnedRootConfig = tasks.register("generatePinnedRootNetworkSecurityConfig", PinnedRootConfigTask::class.java) {
|
||||||
|
description = "Writes the pinned-root network security config for kvitt.publicHost (SPEC §10.5.3)."
|
||||||
|
template.set(layout.projectDirectory.file("src/releasePinnedRoot/network_security_config.xml"))
|
||||||
|
host.set(publicHost)
|
||||||
|
rootCa.set(layout.projectDirectory.file("src/release/res/raw/kvitt_root_ca.crt"))
|
||||||
|
}
|
||||||
|
androidComponents.onVariants(androidComponents.selector().withBuildType("release")) { variant ->
|
||||||
|
variant.sources.res?.addGeneratedSourceDirectory(pinnedRootConfig, PinnedRootConfigTask::outputDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@
|
|||||||
<data android:scheme="kvitt" android:host="invite" />
|
<data android:scheme="kvitt" android:host="invite" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Unverified link: the user may get a chooser (AC-2.6). The host is the Gradle property kvitt.publicHost
|
<!-- Unverified link: the user may get a chooser (AC-2.6). The host is the Gradle property kvitt.publicHost
|
||||||
(BuildConfig.PUBLIC_HOST, D-121). -->
|
(BuildConfig.PUBLIC_HOST, D-121); no port, since Kvitt is served on 443 (D-124). A filter without a
|
||||||
|
port matches every port of the host. -->
|
||||||
<intent-filter android:autoVerify="false">
|
<intent-filter android:autoVerify="false">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -43,7 +44,6 @@
|
|||||||
<data
|
<data
|
||||||
android:scheme="https"
|
android:scheme="https"
|
||||||
android:host="${publicHost}"
|
android:host="${publicHost}"
|
||||||
android:port="8443"
|
|
||||||
android:pathPrefix="/i/" />
|
android:pathPrefix="/i/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ sealed interface AppLink {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the intents `MainActivity` receives: `kvitt://invite?server=…&code=…[&from=…]` and
|
* Parses the intents `MainActivity` receives: `kvitt://invite?server=…&code=…[&from=…]` and
|
||||||
* `https://<BuildConfig.PUBLIC_HOST>:8443/i/<CODE>[?from=…]` (AC-2.6; the manifest's intent filter takes its host from the
|
* `https://<BuildConfig.PUBLIC_HOST>/i/<CODE>[?from=…]` (AC-2.6, D-124; the manifest's intent filter takes its host from
|
||||||
* `publicHost` placeholder, D-121), both through `:feature:onboarding`'s [InviteLink], the reader S-1 uses for pasted
|
* the `publicHost` placeholder, D-121), both through `:feature:onboarding`'s [InviteLink], the reader S-1 uses for pasted
|
||||||
* links, which takes the server from the link itself, and the notification taps this app builds ([openIntent]).
|
* links, which takes the server from the link itself, and the notification taps this app builds ([openIntent]).
|
||||||
*/
|
*/
|
||||||
object AppLinks {
|
object AppLinks {
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import se.hagstedt.kvitt.BuildConfig
|
|||||||
import se.hagstedt.kvitt.feature.onboarding.releaseServerUrl
|
import se.hagstedt.kvitt.feature.onboarding.releaseServerUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The release server, `https://<BuildConfig.PUBLIC_HOST>:8443` (AC-2.7, D-121): prefilled on S-1 and the host of the
|
* The release server, `https://<BuildConfig.PUBLIC_HOST>` on port 443 (AC-2.7, D-121, D-124): prefilled on S-1 and the
|
||||||
* `https` invite links the manifest declares (AC-2.6, the `publicHost` placeholder). `PUBLIC_HOST` is the Gradle
|
* host of the `https` invite links the manifest declares (AC-2.6, the `publicHost` placeholder). `PUBLIC_HOST` is the
|
||||||
* property `kvitt.publicHost`; without it the build uses the placeholder `kvitt.example.invalid`, which never resolves.
|
* Gradle property `kvitt.publicHost`; without it the build uses the placeholder `kvitt.example.invalid`, which never
|
||||||
|
* resolves.
|
||||||
*/
|
*/
|
||||||
val RELEASE_SERVER_URL: String = releaseServerUrl(BuildConfig.PUBLIC_HOST)
|
val RELEASE_SERVER_URL: String = releaseServerUrl(BuildConfig.PUBLIC_HOST)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- SPEC §10.5.3 fallback, used only with kvitt.tls.pinnedRoot=true: the Caddy internal root (committed as
|
<!-- SPEC §10.5.3 fallback, used only with kvitt.tls.pinnedRoot=true (D-082): a self-made root behind NPM (D-120),
|
||||||
src/release/res/raw/kvitt_root_ca.crt) replaces the system anchors for icehack.asuscomm.com only. -->
|
committed as src/release/res/raw/kvitt_root_ca.crt when the fallback is prepared, replaces the system anchors for
|
||||||
|
Kvitt's public host only. Template: the build writes it into the release resources with @PUBLIC_HOST@ replaced
|
||||||
|
by the Gradle property kvitt.publicHost (D-121); see generatePinnedRootNetworkSecurityConfig in build.gradle.kts. -->
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
<base-config cleartextTrafficPermitted="false">
|
<base-config cleartextTrafficPermitted="false">
|
||||||
<trust-anchors>
|
<trust-anchors>
|
||||||
@@ -8,7 +10,7 @@
|
|||||||
</trust-anchors>
|
</trust-anchors>
|
||||||
</base-config>
|
</base-config>
|
||||||
<domain-config cleartextTrafficPermitted="false">
|
<domain-config cleartextTrafficPermitted="false">
|
||||||
<domain includeSubdomains="false">icehack.asuscomm.com</domain>
|
<domain includeSubdomains="false">@PUBLIC_HOST@</domain>
|
||||||
<trust-anchors>
|
<trust-anchors>
|
||||||
<certificates src="@raw/kvitt_root_ca" />
|
<certificates src="@raw/kvitt_root_ca" />
|
||||||
</trust-anchors>
|
</trust-anchors>
|
||||||
@@ -23,10 +23,11 @@ class AppLinksTest {
|
|||||||
private val host = BuildConfig.PUBLIC_HOST
|
private val host = BuildConfig.PUBLIC_HOST
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun theReleaseServerIsHttpsOnPort8443OfThePublicHost() {
|
fun theReleaseServerIsHttpsOnTheDefaultPortOfThePublicHost() {
|
||||||
assertEquals("https://$host:8443", RELEASE_SERVER_URL)
|
// D-124: port 443, written without a port.
|
||||||
|
assertEquals("https://$host", RELEASE_SERVER_URL)
|
||||||
// Without kvitt.publicHost the build uses the placeholder, which never resolves (RFC 2606).
|
// Without kvitt.publicHost the build uses the placeholder, which never resolves (RFC 2606).
|
||||||
assertEquals("https://kvitt.example.invalid:8443", releaseServerUrl("kvitt.example.invalid"))
|
assertEquals("https://kvitt.example.invalid", releaseServerUrl("kvitt.example.invalid"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -37,8 +38,13 @@ class AppLinksTest {
|
|||||||
0,
|
0,
|
||||||
).any { it.activityInfo.name == "se.hagstedt.kvitt.MainActivity" }
|
).any { it.activityInfo.name == "se.hagstedt.kvitt.MainActivity" }
|
||||||
assertTrue(opens("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN"))
|
assertTrue(opens("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN"))
|
||||||
assertFalse(opens("https://other.example.org:8443/i/ABCDEFGHJKMN"))
|
assertTrue(opens("https://$host/i/ABCDEFGHJKMN?from=Jacob"))
|
||||||
assertFalse(opens("https://$host/i/ABCDEFGHJKMN"))
|
assertFalse(opens("https://other.example.org/i/ABCDEFGHJKMN"))
|
||||||
|
assertFalse(opens("http://$host/i/ABCDEFGHJKMN"))
|
||||||
|
assertFalse(opens("https://$host/apk/latest"))
|
||||||
|
// A filter without a port matches every port of the host (android:port="443" would match no link without an
|
||||||
|
// explicit port), so an old :8443 link still opens S-1, with the server the link names (AC-2.6).
|
||||||
|
assertTrue(opens("https://$host:8443/i/ABCDEFGHJKMN"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -50,13 +56,15 @@ class AppLinksTest {
|
|||||||
@Test
|
@Test
|
||||||
fun appLinkNormalisesTheCode() {
|
fun appLinkNormalisesTheCode() {
|
||||||
// §8.3: lowercase, O -> 0, I and L -> 1.
|
// §8.3: lowercase, O -> 0, I and L -> 1.
|
||||||
assertEquals("ABCD0113EFGH", AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2F$host%3A8443&code=abcdo1l3efgh")?.code)
|
assertEquals("ABCD0113EFGH", AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2F$host&code=abcdo1l3efgh")?.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun landingLinkFillsTheServerFromTheLink() {
|
fun landingLinkFillsTheServerFromTheLink() {
|
||||||
val link = AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")
|
val link = AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")
|
||||||
assertEquals(AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN"), link)
|
assertEquals(AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN"), link)
|
||||||
|
// An explicit default port names the same server.
|
||||||
|
assertEquals(AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN"), AppLinks.parseUri("https://$host:443/i/ABCDEFGHJKMN"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -67,7 +75,7 @@ class AppLinksTest {
|
|||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
AppLink.Invite(RELEASE_SERVER_URL, "ABCDEFGHJKMN", "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"),
|
AppLinks.parseUri("kvitt://invite?server=https%3A%2F%2F${host.replace(".", "%2E")}&code=ABCDEFGHJKMN&from=Jacob"),
|
||||||
)
|
)
|
||||||
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")?.adminName)
|
assertNull(AppLinks.parseUri("$RELEASE_SERVER_URL/i/ABCDEFGHJKMN")?.adminName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ object InviteLink {
|
|||||||
val segments = path.trim('/').split('/')
|
val segments = path.trim('/').split('/')
|
||||||
if (segments.size != 2 || segments[0] != "i") return null
|
if (segments.size != 2 || segments[0] != "i") return null
|
||||||
val code = code(segments[1]) ?: return null
|
val code = code(segments[1]) ?: return null
|
||||||
val port = if (uri.port == -1) "" else ":${uri.port}"
|
val scheme = uri.scheme.lowercase()
|
||||||
return PastedInvite(code, "${uri.scheme.lowercase()}://${uri.host}$port", adminName(uri.rawQuery?.let(::params)?.get(FROM)))
|
// 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. */
|
/** 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 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. */
|
/** The normalised code when [text] normalises to exactly 12 alphabet characters (§8.3), else null. */
|
||||||
private fun code(text: String): String? {
|
private fun code(text: String): String? {
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import java.net.URI
|
|||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import javax.inject.Qualifier
|
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
|
* The release server of a public host name, `https://<host>` on the default port 443, where NPM selects Kvitt by host
|
||||||
* `BuildConfig.PUBLIC_HOST` (the Gradle property `kvitt.publicHost`) and provides it as [ReleaseServerUrl].
|
* 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). */
|
/** The release server prefilled on S-1 (AC-2.7), `RELEASE_SERVER_URL` in `:app` (D-121). */
|
||||||
@Qualifier
|
@Qualifier
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class InviteLinkTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun theAppLinkGivesCodeAndServer() {
|
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)
|
assertEquals(PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL), parsed)
|
||||||
assertNull(InviteLink.parse("kvitt://other?code=7K3MQX9D2HPA"))
|
assertNull(InviteLink.parse("kvitt://other?code=7K3MQX9D2HPA"))
|
||||||
}
|
}
|
||||||
@@ -33,21 +33,28 @@ class InviteLinkTest {
|
|||||||
fun linksMayNameTheAdmin_andABadNameIsDropped() {
|
fun linksMayNameTheAdmin_andABadNameIsDropped() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL, "Jacob"),
|
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(
|
assertEquals(
|
||||||
PastedInvite("7K3MQX9D2HPA", TEST_RELEASE_SERVER_URL, "Åsa Berg"),
|
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))) {
|
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
|
@Test
|
||||||
fun theLandingLinkGivesCodeAndServer() {
|
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"))
|
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
|
@Test
|
||||||
fun pasteFillsCodeAndServerOrSaysTheCodeHasTwelveCharacters() {
|
fun pasteFillsCodeAndServerOrSaysTheCodeHasTwelveCharacters() {
|
||||||
val (_, vm) = vm()
|
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)
|
assertEquals("7K3MQX9D2HPA", vm.state.value.code)
|
||||||
assertNull(vm.state.value.error)
|
assertNull(vm.state.value.error)
|
||||||
vm.onPaste("hello")
|
vm.onPaste("hello")
|
||||||
|
|||||||
@@ -42,10 +42,11 @@ class ServerAddressTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun theReleaseServerIsHttpsOnPort8443OfThePublicHost() {
|
fun theReleaseServerIsHttpsOnTheDefaultPortOfThePublicHost() {
|
||||||
// D-121: the build's default kvitt.publicHost is the placeholder, which never resolves (RFC 2606).
|
// D-121: the build's default kvitt.publicHost is the placeholder, which never resolves (RFC 2606); D-124: port 443,
|
||||||
assertEquals("https://kvitt.example.invalid:8443", releaseServerUrl("kvitt.example.invalid"))
|
// written without a port.
|
||||||
assertEquals("https://kvitt.duckdns.org:8443", releaseServerUrl("kvitt.duckdns.org"))
|
assertEquals("https://kvitt.example.invalid", releaseServerUrl("kvitt.example.invalid"))
|
||||||
|
assertEquals("https://kvitt.duckdns.org", releaseServerUrl("kvitt.duckdns.org"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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
|
* 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
|
* 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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 34 KiB |