test(e2e): accept the scan fixture's raw bytes as Base64 text
android / android (push) Failing after 4m0s
build / android (push) Failing after 4m0s

adb shell am broadcast cannot send a byte-array extra, so a manual emulator
scan could not carry the raw bytes a GS1-128 duplication needs. The E2E
driver now also reads io.qrun.mobile.scanner.e2e.raw_data_base64 (canonical
padded Base64). The byte-array extra still wins when both are present.
Malformed Base64, and an invalid AIM identifier (which used to throw inside
the receiver), now leave the read pending so the broadcast is ignored.

E2E source set only: no change to main, debug, qa, production or release.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Fable 5.2 committed 2026-11-10 10:36:00 -06:00
1 parent 9bae976138
commit d5c3ac2d6a
3 files changed
+75 -5

No files matched your search

+9 -1
View File
@@ -41,7 +41,15 @@ Only the disposable E2E APK accepts synthetic broadcasts while its scanner scree
adb shell am broadcast -a io.qrun.mobile.scanner.SCAN -c android.intent.category.DEFAULT -p io.qrun.mobile.scanner.e2e --es com.symbol.datawedge.data_string DEMO-001 --es com.symbol.datawedge.label_type LABEL-TYPE-CODE128
```
This validates intent parsing only; the server treats source/symbology as client-reported metadata.
`am broadcast` cannot send a byte-array extra. A scan that needs fidelity metadata (for example GS1-128) adds the AIM identifier and the raw bytes as canonical padded Base64 text, both quoted for the device shell:
```text
--es io.qrun.mobile.scanner.e2e.aim_identifier ']C1' --es io.qrun.mobile.scanner.e2e.raw_data_base64 'XUMxMDEwOTUwMTEwMTUzMDAwMzExMjAxMDE0MjFURVNUMDAwMTIz'
```
The byte-array extra `io.qrun.mobile.scanner.e2e.raw_data` wins when both are present. A broadcast with malformed Base64 or an invalid AIM identifier is ignored and the read stays pending. `am to-intent-uri` with the same arguments prints how the device parsed them.
This validates intent parsing only; the server treats source/symbology as client-reported metadata. The E2E package signs in with its own callback (`io.qrun.mobile.scanner.e2e.oauth://callback/oauth2redirect`, see [AUTHENTICATION.md](AUTHENTICATION.md)), so a server that advertises only the normal scanner callback offers it no browser sign-in.
## Automated checks
@@ -6,11 +6,21 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import java.util.Base64
internal const val HARDWARE_SCAN_SOURCE = "e2e-fixture"
internal const val E2E_RAW_DATA_EXTRA = "io.qrun.mobile.scanner.e2e.raw_data"
internal const val E2E_RAW_DATA_BASE64_EXTRA = "io.qrun.mobile.scanner.e2e.raw_data_base64"
internal const val E2E_AIM_IDENTIFIER_EXTRA = "io.qrun.mobile.scanner.e2e.aim_identifier"
/*******************************************************************************
** `adb shell am broadcast` cannot send a byte-array extra, so a fixture may send the same bytes as canonical
** padded Base64 text. The byte array wins when both are present; other text throws IllegalArgumentException.
*******************************************************************************/
internal fun e2eRawData(raw: ByteArray?, base64: String?): ByteArray? = raw ?: base64?.let { text ->
Base64.getDecoder().decode(text).also { require(Base64.getEncoder().encodeToString(it) == text) { "Raw data must be canonical padded Base64." } }
}
/*******************************************************************************
** Disposable E2E package only. No debug/release source set contains this ingress.
*******************************************************************************/
@@ -26,11 +36,17 @@ internal fun createScannerDriver(context: Context): ScannerDriver = object : Sca
if (!pending || intent?.action != "io.qrun.mobile.scanner.SCAN") return
val data = intent.getStringExtra("com.symbol.datawedge.data_string") ?: return
if (intent.getStringExtra("com.symbol.datawedge.decode_mode") == "multiple" || intent.hasExtra("com.symbol.datawedge.barcodes")) return
pending = false
// Test-only fixture metadata: exact bytes and an explicit AIM identifier, never derived from the text.
val raw = intent.getByteArrayExtra(E2E_RAW_DATA_EXTRA)
val aim = intent.getStringExtra(E2E_AIM_IDENTIFIER_EXTRA)
listener?.scanned(ScannerCapture.of(data, intent.getStringExtra("com.symbol.datawedge.label_type") ?: "UNKNOWN", raw, aim))
// Malformed metadata leaves the read pending, so the broadcast is ignored like any other invalid one.
val capture = try {
val raw = e2eRawData(intent.getByteArrayExtra(E2E_RAW_DATA_EXTRA), intent.getStringExtra(E2E_RAW_DATA_BASE64_EXTRA))
val aim = intent.getStringExtra(E2E_AIM_IDENTIFIER_EXTRA)
ScannerCapture.of(data, intent.getStringExtra("com.symbol.datawedge.label_type") ?: "UNKNOWN", raw, aim)
} catch (invalid: IllegalArgumentException) {
return
}
pending = false
listener?.scanned(capture)
listener?.idle()
}
}
@@ -0,0 +1,46 @@
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
package io.qrun.mobile.scanner
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertThrows
import org.junit.Test
/*******************************************************************************
** The E2E fixture's text alternative for raw bytes, which `adb shell am broadcast` can send.
*******************************************************************************/
class E2eRawDataTest {
private val framed = "]C101095011015300031120101421TEST000123"
/*******************************************************************************
** The decoded text reaches the capture as the same canonical Base64 the byte-array extra produces.
*******************************************************************************/
@Test
fun base64TextDecodesToTheExactBytes() {
val text = java.util.Base64.getEncoder().encodeToString(framed.toByteArray(Charsets.US_ASCII))
assertArrayEquals(framed.toByteArray(Charsets.US_ASCII), e2eRawData(null, text))
assertEquals(text, ScannerCapture.of(framed.substring(3), "LABEL-TYPE-EAN128", e2eRawData(null, text), "]C1").rawDataBase64)
assertArrayEquals(byteArrayOf(0x00, 0xFF.toByte(), 0x80.toByte()), e2eRawData(null, "AP+A"))
}
/*******************************************************************************
**
*******************************************************************************/
@Test
fun byteArrayWinsAndAbsenceStaysAbsent() {
assertArrayEquals(byteArrayOf(1, 2), e2eRawData(byteArrayOf(1, 2), "AP+A"))
assertArrayEquals(byteArrayOf(1, 2), e2eRawData(byteArrayOf(1, 2), "not base64"))
assertNull(e2eRawData(null, null))
}
/*******************************************************************************
** Anything but canonical padded Base64 is refused, so the driver ignores that broadcast.
*******************************************************************************/
@Test
fun malformedOrNonCanonicalTextIsRefused() {
for (text in listOf("not base64", "AP+A ", " AP+A", "AP-A", "QQ", "QQ=", "QR==", "AP+A\n")) {
assertThrows(text, IllegalArgumentException::class.java) { e2eRawData(null, text) }
}
}
}