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 <noreply@anthropic.com>
This commit is contained in:
1 parent
9bae976138
commit
d5c3ac2d6a
3 files changed
+75
-5
No files matched your search
+9
-1
@@ -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
|
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
|
## Automated checks
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,21 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
internal const val HARDWARE_SCAN_SOURCE = "e2e-fixture"
|
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_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"
|
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.
|
** 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
|
if (!pending || intent?.action != "io.qrun.mobile.scanner.SCAN") return
|
||||||
val data = intent.getStringExtra("com.symbol.datawedge.data_string") ?: 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
|
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.
|
// Test-only fixture metadata: exact bytes and an explicit AIM identifier, never derived from the text.
|
||||||
val raw = intent.getByteArrayExtra(E2E_RAW_DATA_EXTRA)
|
// Malformed metadata leaves the read pending, so the broadcast is ignored like any other invalid one.
|
||||||
val aim = intent.getStringExtra(E2E_AIM_IDENTIFIER_EXTRA)
|
val capture = try {
|
||||||
listener?.scanned(ScannerCapture.of(data, intent.getStringExtra("com.symbol.datawedge.label_type") ?: "UNKNOWN", raw, aim))
|
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()
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user