Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d6572bc33 |
@@ -0,0 +1,71 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package app.echo_lot.engine
|
||||||
|
|
||||||
|
import app.echo_lot.protocol.Compat
|
||||||
|
import app.echo_lot.protocol.ControlClient
|
||||||
|
import app.echo_lot.protocol.VersionRefused
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the version gate against a LIVE server — the half that unit tests cannot reach, because
|
||||||
|
* the whole point is that two independently-built artifacts agree. Self-skips without
|
||||||
|
* ECHOLOT_LIVE_*.
|
||||||
|
*/
|
||||||
|
class LiveCompatTest {
|
||||||
|
|
||||||
|
private val url = System.getenv("ECHOLOT_LIVE_URL")
|
||||||
|
private val pin = System.getenv("ECHOLOT_LIVE_PIN")
|
||||||
|
private val cred = System.getenv("ECHOLOT_LIVE_CRED")
|
||||||
|
|
||||||
|
private fun clientAs(version: String) = ControlClient(url!!, setOf(pin!!), version)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theServerAdvertisesAndEnforcesItsWindow() {
|
||||||
|
if (url == null || pin == null || cred == null) {
|
||||||
|
println("LiveCompatTest skipped (no ECHOLOT_LIVE_* env)"); return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The profile must state the window — without it the app cannot pre-empt a refusal.
|
||||||
|
val profile = clientAs("0.2.0").profile(cred)
|
||||||
|
println("server ${profile.serverVersion} protocol=${profile.compat.protocolVersion} " +
|
||||||
|
"accepts app [${profile.compat.appMin}, ${profile.compat.appMax})")
|
||||||
|
assertTrue(profile.compat.protocolVersion.isNotBlank(), "profile omits protocol_version")
|
||||||
|
assertTrue(profile.compat.appMin.isNotBlank(), "profile omits app_min")
|
||||||
|
|
||||||
|
// This build must be inside it, or every other live test here is meaningless.
|
||||||
|
val verdict = Compat.check(profile, "0.2.0")
|
||||||
|
assertEquals(Compat.Verdict.OK, verdict.verdict, verdict.message ?: "")
|
||||||
|
|
||||||
|
// The profile stays reachable for a version the server would otherwise refuse: that is
|
||||||
|
// how a refused client discovers what it needs.
|
||||||
|
val ancient = clientAs("0.1.0")
|
||||||
|
val stillReadable = ancient.profile(cred)
|
||||||
|
assertEquals(profile.serverVersion, stillReadable.serverVersion,
|
||||||
|
"the profile endpoint must never be gated on app version")
|
||||||
|
|
||||||
|
// And a gated endpoint refuses it, with a message naming the window.
|
||||||
|
try {
|
||||||
|
ancient.createSession(cred, System.getenv("ECHOLOT_LIVE_TARGET") ?: "fmr")
|
||||||
|
fail("server accepted a session from an out-of-window app")
|
||||||
|
} catch (e: VersionRefused) {
|
||||||
|
val msg = assertNotNull(e.message)
|
||||||
|
println("refused as expected: $msg")
|
||||||
|
assertTrue(msg.contains("0.1.0"), "refusal should name the offending version: $msg")
|
||||||
|
assertTrue(msg.contains(profile.compat.appMin), "refusal should name the window: $msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Too new is refused the same way — the window is a range, not a floor.
|
||||||
|
try {
|
||||||
|
clientAs("99.0.0").createSession(cred, System.getenv("ECHOLOT_LIVE_TARGET") ?: "fmr")
|
||||||
|
fail("server accepted a session from an app above its window")
|
||||||
|
} catch (e: VersionRefused) {
|
||||||
|
println("too-new refused as expected: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -183,7 +183,7 @@ func Check(peer string, r Range, peerName string) (Verdict, string) {
|
|||||||
return TooOld, fmt.Sprintf("%s %s is older than this build supports (needs %s). Update the %s.",
|
return TooOld, fmt.Sprintf("%s %s is older than this build supports (needs %s). Update the %s.",
|
||||||
peerName, v, r, peerName)
|
peerName, v, r, peerName)
|
||||||
case r.HasMax && !v.Less(r.Max):
|
case r.HasMax && !v.Less(r.Max):
|
||||||
return TooNew, fmt.Sprintf("%s %s is newer than this build supports (accepts %s). Update this side, or point at a %s within range.",
|
return TooNew, fmt.Sprintf("%s %s is newer than this build supports (accepts %s). Update this side, or use a version of the %s within that range.",
|
||||||
peerName, v, r, peerName)
|
peerName, v, r, peerName)
|
||||||
}
|
}
|
||||||
return OK, ""
|
return OK, ""
|
||||||
|
|||||||
Reference in New Issue
Block a user