Test the roster gate and the own-account announcement directly
Two gaps left by the group-name work, both in the half that decides whether a claim is believed. The roster check that turns a group-name CLAIM into a stored name lives in Session, and was covered only through Conversations — which asserts the claim is NOT adopted there and says nothing about what happens next. iOS had its equivalent tested directly; Android's was a layer removed. It now has four tests: a member is adopted, a non-member is refused, a gid we are not in is refused, and a refused claim is consumed rather than left to retry against a roster that might later differ. Removing the membership test fails two of them. Reaching it needed a seam: `consumeControl` is internal to this module, on purpose, and driving it through a real ratchet would mean making a real sender claim a membership it does not have — which a real sender cannot be made to do. `consumeControlForTesting` says that in its own comment. The second gap: the announcement addresses our own account so a phone we own is not told by nobody, and nothing checked that it did. Removing our account from the list now fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2b75f0e41b
commit
556580f4fb
@@ -70,6 +70,22 @@ class Conversations(
|
||||
*/
|
||||
private val maxClaimedGroupNames = 256
|
||||
|
||||
/**
|
||||
* ⚠️ **Test seam, and the reason it exists is specific.** The roster check
|
||||
* that turns one of these claims into a stored name lives in `Session`, a
|
||||
* module up, so the test that proves a non-member is refused cannot reach
|
||||
* `consumeControl` — it is `internal` here, deliberately, because it is the
|
||||
* seam where a control envelope becomes a stored fact.
|
||||
*
|
||||
* Driving it through a real ratchet instead would mean standing up two
|
||||
* parties and then making one of them claim a membership it does not have,
|
||||
* which a real sender cannot be made to do. So the refusal is exercised by
|
||||
* handing this layer the shape an attacker would produce, exactly as the
|
||||
* iOS side does with its own receive path.
|
||||
*/
|
||||
fun consumeControlForTesting(fromUser: Long, content: Content) =
|
||||
consumeControl(fromUser, content)
|
||||
|
||||
/** Take the pending claims, leaving none behind. */
|
||||
fun takeClaimedGroupNames(): List<Triple<Long, String, String>> =
|
||||
synchronized(claimedGroupNames) {
|
||||
|
||||
@@ -313,6 +313,46 @@ class GroupNameTest {
|
||||
assertFalse(unproven.senderProven)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 **The announcement goes to our own account too, and that is the point.**
|
||||
* Fan-out excludes us everywhere else, so a second phone we own would be
|
||||
* told by nobody: the relay has no name to serve it and the members only
|
||||
* send to each other.
|
||||
*
|
||||
* ⚠️ Safe only because the fan-out loop already skips our own device
|
||||
* ("never talk to ourselves"). This asserts the recipient list, which is
|
||||
* the half this file owns; the device-level skip has its own guard in the
|
||||
* send path.
|
||||
*/
|
||||
@Test
|
||||
fun `the announcement addresses our own account as well as the members`() {
|
||||
val vault = vault()
|
||||
vault.saveDelivery(Vault.Delivery(key = ByteArray(32) { 3 }, published = true))
|
||||
vault.saveGroupName("g1", "Book club")
|
||||
val conversations = conversations(vault).also { it.myUserId = 1L }
|
||||
|
||||
val asked = mutableListOf<Long>()
|
||||
// Every send starts by listing the recipient's devices, so the paths
|
||||
// the relay is asked for name exactly who was addressed.
|
||||
server.dispatcher = object : okhttp3.mockwebserver.Dispatcher() {
|
||||
override fun dispatch(request: okhttp3.mockwebserver.RecordedRequest):
|
||||
MockResponse {
|
||||
val path = request.path.orEmpty()
|
||||
Regex("/e2e/devices/(\\d+)").find(path)?.let {
|
||||
asked += it.groupValues[1].toLong()
|
||||
}
|
||||
return MockResponse().setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("""{"user_id":0,"devices":[]}""")
|
||||
}
|
||||
}
|
||||
|
||||
conversations.announceGroupName("g1", listOf(2L, 3L))
|
||||
|
||||
assertTrue(1L in asked, "our own account was never addressed: $asked")
|
||||
assertTrue(2L in asked && 3L in asked, "a member was missed: $asked")
|
||||
}
|
||||
|
||||
// -- Where it is kept ------------------------------------------------------
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user