From 72847db28bbcc1129d748bfcb1fff4b83e341035 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Apr 2026 20:33:16 +0900 Subject: [PATCH] test: cover android canvas a2ui trust gate --- .../ai/openclaw/app/node/CanvasActionTrust.kt | 6 ++- .../java/ai/openclaw/app/ui/CanvasScreen.kt | 2 +- .../app/ui/CanvasA2UIActionBridgeTest.kt | 50 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 apps/android/app/src/test/java/ai/openclaw/app/ui/CanvasA2UIActionBridgeTest.kt diff --git a/apps/android/app/src/main/java/ai/openclaw/app/node/CanvasActionTrust.kt b/apps/android/app/src/main/java/ai/openclaw/app/node/CanvasActionTrust.kt index 5cdf12a7496..4e909121806 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/node/CanvasActionTrust.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/node/CanvasActionTrust.kt @@ -17,17 +17,19 @@ object CanvasActionTrust { val normalizedCandidate = normalizeTrustedRemoteA2uiUri(candidateUri) ?: return false return trustedA2uiUrls.any { trusted -> - isTrustedA2uiPage(normalizedCandidate, trusted) + matchesTrustedRemoteA2uiUrlExact(normalizedCandidate, trusted) } } - private fun isTrustedA2uiPage(candidateUri: URI, trustedUrl: String): Boolean { + private fun matchesTrustedRemoteA2uiUrlExact(candidateUri: URI, trustedUrl: String): Boolean { val trustedUri = parseUri(trustedUrl) ?: return false val normalizedTrusted = normalizeTrustedRemoteA2uiUri(trustedUri) ?: return false return candidateUri == normalizedTrusted } private fun normalizeTrustedRemoteA2uiUri(uri: URI): URI? { + // Keep Android trust normalization aligned with iOS ScreenController: + // exact remote URL match, scheme/host normalized, fragment ignored. val scheme = uri.scheme?.lowercase() ?: return null if (scheme != "http" && scheme != "https") return null diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/CanvasScreen.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/CanvasScreen.kt index cfd635d8fa0..e299dea2371 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/CanvasScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/CanvasScreen.kt @@ -163,7 +163,7 @@ private fun disableForceDarkIfSupported(settings: WebSettings) { WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF) } -private class CanvasA2UIActionBridge( +internal class CanvasA2UIActionBridge( private val isTrustedPage: () -> Boolean, private val onMessage: (String) -> Unit, ) { diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/CanvasA2UIActionBridgeTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/CanvasA2UIActionBridgeTest.kt new file mode 100644 index 00000000000..c9b12fc1296 --- /dev/null +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/CanvasA2UIActionBridgeTest.kt @@ -0,0 +1,50 @@ +package ai.openclaw.app.ui + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class CanvasA2UIActionBridgeTest { + @Test + fun forwardsTrimmedPayloadFromTrustedPage() { + val forwarded = mutableListOf() + val bridge = + CanvasA2UIActionBridge( + isTrustedPage = { true }, + onMessage = { forwarded += it }, + ) + + bridge.postMessage(" {\"ok\":true} ") + + assertEquals(listOf("{\"ok\":true}"), forwarded) + } + + @Test + fun rejectsPayloadFromUntrustedPage() { + val forwarded = mutableListOf() + val bridge = + CanvasA2UIActionBridge( + isTrustedPage = { false }, + onMessage = { forwarded += it }, + ) + + bridge.postMessage("{\"ok\":true}") + + assertTrue(forwarded.isEmpty()) + } + + @Test + fun rejectsBlankPayloadBeforeForwarding() { + val forwarded = mutableListOf() + val bridge = + CanvasA2UIActionBridge( + isTrustedPage = { true }, + onMessage = { forwarded += it }, + ) + + bridge.postMessage(" ") + bridge.postMessage(null) + + assertTrue(forwarded.isEmpty()) + } +}