From d5c3ac2d6af8e4d43fac0b67454805e1d675318a Mon Sep 17 00:00:00 2001 From: James Maes Date: Tue, 10 Nov 2026 10:36:00 -0600 Subject: [PATCH] test(e2e): accept the scan fixture's raw bytes as Base64 text 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 --- docs/EMULATOR.md | 10 +++- .../mobile/scanner/ScannerDriverFactory.kt | 24 ++++++++-- .../io/qrun/mobile/scanner/E2eRawDataTest.kt | 46 +++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 scanner-host/src/testE2e/java/io/qrun/mobile/scanner/E2eRawDataTest.kt diff --git a/docs/EMULATOR.md b/docs/EMULATOR.md index 80e60e5..2eac124 100644 --- a/docs/EMULATOR.md +++ b/docs/EMULATOR.md @@ -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 diff --git a/scanner-host/src/e2e/java/io/qrun/mobile/scanner/ScannerDriverFactory.kt b/scanner-host/src/e2e/java/io/qrun/mobile/scanner/ScannerDriverFactory.kt index 71922c6..faa174c 100644 --- a/scanner-host/src/e2e/java/io/qrun/mobile/scanner/ScannerDriverFactory.kt +++ b/scanner-host/src/e2e/java/io/qrun/mobile/scanner/ScannerDriverFactory.kt @@ -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() } } diff --git a/scanner-host/src/testE2e/java/io/qrun/mobile/scanner/E2eRawDataTest.kt b/scanner-host/src/testE2e/java/io/qrun/mobile/scanner/E2eRawDataTest.kt new file mode 100644 index 0000000..3f16535 --- /dev/null +++ b/scanner-host/src/testE2e/java/io/qrun/mobile/scanner/E2eRawDataTest.kt @@ -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) } + } + } +}