fix(android): AVTransport test assertions escape DIDL; consolidate xmlEscape
android / Build + lint + test (push) Failing after 1m18s

DIDL assertion now checks for XML-escaped form (<dc:title>) since
SoapClient.buildEnvelope escapes all arg values. Lifts xmlEscape to a
top-level internal fun in SoapClient.kt, removing the duplicate private
copy from AVTransportClient. Fixes @Suppress rationale (not Compose).
Renames seek test to reflect colon-separated format; adds unknown-state
getTransportInfo test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:47:18 -04:00
parent d62a3b8134
commit 3cdb416f94
3 changed files with 34 additions and 18 deletions
@@ -7,7 +7,8 @@ import okhttp3.HttpUrl
* Covers SetAVTransportURI / SetNextAVTransportURI / Play / Pause /
* Stop / Seek / GetPositionInfo / GetTransportInfo.
*/
@Suppress("TooManyFunctions") // Compose-adjacent helper density: all are direct SOAP verbs.
// One method per AVTransport SOAP verb; splitting would obscure the 1:1 protocol mapping.
@Suppress("TooManyFunctions")
class AVTransportClient(
private val soap: SoapClient,
private val controlUrl: HttpUrl,
@@ -151,13 +152,6 @@ class AVTransportClient(
}
}
private fun xmlEscape(v: String): String = v
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
private fun formatHhMmSs(positionMs: Long): String {
val totalSec = (positionMs / MILLIS_PER_SECOND).coerceAtLeast(0)
val h = totalSec / SECONDS_PER_HOUR
@@ -69,13 +69,6 @@ class SoapClient(
append("</s:Envelope>")
}
private fun xmlEscape(v: String): String = v
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
private fun parseResponseArgs(body: String, action: String): Map<String, String> {
val parser = XmlPullParserFactory.newInstance().newPullParser().apply {
setInput(body.reader())
@@ -138,3 +131,11 @@ class SoapClient(
*/
class SoapFaultException(val code: String, val description: String) :
Exception("SOAP fault $code: $description")
/** XML-escapes a string value for embedding as text content inside a SOAP envelope. */
internal fun xmlEscape(v: String): String = v
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
@@ -29,7 +29,7 @@ class AVTransportClientTest {
}
@Test
fun `seek formats position as hh-mm-ss`() = runTest {
fun `seek sends Target in HH MM SS format`() = runTest {
server.enqueue(emptyResponse("Seek"))
client.seek(positionMs = 65_000L)
val body = server.takeRequest().body.readUtf8()
@@ -81,6 +81,27 @@ class AVTransportClientTest {
assertEquals(TransportState.PAUSED, info.state)
}
@Test
fun `getTransportInfo maps unknown state to UNKNOWN`() = runTest {
server.enqueue(
MockResponse().setBody(
"""<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:GetTransportInfoResponse
xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<CurrentTransportState>BUFFERING_PLAYBACK</CurrentTransportState>
<CurrentTransportStatus>OK</CurrentTransportStatus>
<CurrentSpeed>1</CurrentSpeed>
</u:GetTransportInfoResponse>
</s:Body>
</s:Envelope>""".trimIndent(),
),
)
val info = client.getTransportInfo()
assertEquals(TransportState.UNKNOWN, info.state)
}
@Test
fun `setNextAVTransportURI sends DIDL-Lite with mime and title`() = runTest {
server.enqueue(emptyResponse("SetNextAVTransportURI"))
@@ -93,8 +114,8 @@ class AVTransportClientTest {
assertTrue(body.contains("<NextURI>http://x/y.mp3</NextURI>")) {
"body missing NextURI: $body"
}
assertTrue(body.contains("<dc:title>Song</dc:title>")) {
"body missing dc:title: $body"
assertTrue(body.contains("&lt;dc:title&gt;Song&lt;/dc:title&gt;")) {
"title missing: $body"
}
assertTrue(body.contains("audio/mpeg")) {
"body missing mime type: $body"