0.26, and a test for the wire the relay and this client agree on

Both halves of 8.9c were written in different repositories on the same
afternoon, and the way that goes wrong is a field name: this client posts
"digests" and the relay reads "tags", both suites stay green, and presence
silently never works. RosterTagWireTest asserts the exact bytes against a mock
server, including the route paths and the token-keyed answer shape.

It also pins two behaviours worth keeping: no tokens means no request at all,
because a relay asked about nothing still costs a round trip and a rate-limit
unit, and a token the relay does not recognise comes back offline rather than
missing, since "offline" and "no answer" are different things to whatever
merges this with a roster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-15 16:40:15 -04:00
co-authored by Claude Opus 5
parent 7ebd519942
commit 89474a1fc7
2 changed files with 116 additions and 2 deletions
+2 -2
View File
@@ -43,8 +43,8 @@ android {
// is a label. Code 24 with name 0.24 closes it by skipping the name
// 0.23, which costs nothing because a name is a label and may skip.
// From here they move together and match: 25 with 0.25, 26 with 0.26.
versionCode = 25
versionName = "0.25"
versionCode = 26
versionName = "0.26"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -0,0 +1,114 @@
package io.flamenet.client
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.json.JSONObject
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* §8.9c, the wire between this client and the relay.
*
* Both sides of this pair were written in different repositories on the same
* afternoon, and the way that goes wrong is a field name: the client posts
* `digests` and the relay reads `tags`, both suites stay green, and presence
* silently never works. This asserts the exact bytes.
*
* The literals here are the relay's, from `PublishRosterTagsRequest` and
* `PresenceByTokenResponse` in `Wire.swift`. Changing one without the other is
* what this is for.
*/
class RosterTagWireTest {
private lateinit var server: MockWebServer
@BeforeTest fun start() {
server = MockWebServer().also { it.start() }
}
@AfterTest fun stop() {
server.shutdown()
}
private fun api() = MessengerApi(server.url("/").toString().trimEnd('/'), "ada", "secret")
private fun ok(body: String) =
MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody(body)
@Test
fun `publishing posts digests to the route the relay mounts`() {
server.enqueue(ok("""{"ok":true}"""))
api().publishRosterTags(listOf("AAA=", "BBB="))
val request = server.takeRequest()
assertEquals("POST", request.method)
assertTrue(
request.path!!.endsWith("/messenger/roster/tags"),
"posted to ${request.path}",
)
val body = JSONObject(request.body.readUtf8())
// `digests`, the relay's own field name. Not `tags`, which is what the
// group route next door calls its equivalent.
val digests = body.getJSONArray("digests")
assertEquals(2, digests.length())
assertEquals("AAA=", digests.getString(0))
}
@Test
fun `asking for presence posts tokens and reads the answer back by token`() {
server.enqueue(
ok(
"""{"presence":[
{"token":"TOKEN-A","presence":"online"},
{"token":"TOKEN-B","presence":"offline"}
]}""",
),
)
val answers = api().presenceByToken(listOf("TOKEN-A", "TOKEN-B"))
val request = server.takeRequest()
assertEquals("POST", request.method)
assertTrue(request.path!!.endsWith("/messenger/presence"), "posted to ${request.path}")
assertEquals(
"TOKEN-A",
JSONObject(request.body.readUtf8()).getJSONArray("tokens").getString(0),
)
// Keyed by the token that asked, because the relay names nobody.
assertEquals("online", answers["TOKEN-A"])
assertEquals("offline", answers["TOKEN-B"])
}
/**
* No request at all rather than one with an empty array. A relay asked
* about nothing still costs a round trip and a rate-limit unit.
*/
@Test
fun `no tokens means no request`() {
assertEquals(emptyMap(), api().presenceByToken(emptyList()))
assertEquals(0, server.requestCount)
}
/**
* A row for a token the relay did not recognise reads as offline, and the
* client must carry that through rather than drop the key: "offline" and
* "no answer" are different things to whatever merges this with a roster.
*/
@Test
fun `an unrecognised token comes back offline, not missing`() {
server.enqueue(ok("""{"presence":[{"token":"NOPE","presence":"offline"}]}"""))
assertEquals("offline", api().presenceByToken(listOf("NOPE"))["NOPE"])
}
/** An older relay has neither route, and that must not throw into the UI. */
@Test
fun `a relay without the route is not fatal`() {
server.enqueue(MockResponse().setResponseCode(404).setBody("""{"error":true}"""))
val failed = runCatching { api().presenceByToken(listOf("TOKEN-A")) }
assertTrue(failed.isFailure, "the API surfaces it; Session is what swallows it")
}
}