fix(#152): verify split saved address evidence
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 35
|
||||
versionName = "0.9.22"
|
||||
versionCode = 36
|
||||
versionName = "0.9.23"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
+29
-6
@@ -73,7 +73,7 @@ class PurchaseLiveAutomation(
|
||||
val snapshot = driver.capture()
|
||||
pageProblem(snapshot)
|
||||
if (snapshot.packageName != PDD_PACKAGE) fail("PURCHASE_CONFIRMATION_LOST", "最终提交前页面已经变化,禁止创建订单")
|
||||
if (snapshot.nodes.none { it.visible && it.label.contains(address.expectedAddress) }) {
|
||||
if (!hasSavedAddressEvidence(snapshot, address.expectedAddress, address.suffix)) {
|
||||
fail("PURCHASE_ADDRESS_UPDATE_FAILED", "收货地址保存后复核失败,未创建订单")
|
||||
}
|
||||
val submit = finalSubmitTargets(snapshot)
|
||||
@@ -171,17 +171,40 @@ class PurchaseLiveAutomation(
|
||||
val save = uniqueClickable(stable, stable.nodes.filter { it.visible && it.enabled && it.label == "保存" })
|
||||
if (save.size != 1) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存按钮不唯一,未创建订单")
|
||||
click(save.single(), "保存地址")
|
||||
var saved = waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存超时,未创建订单") { snapshot ->
|
||||
snapshot.nodes.any { it.visible && it.label.contains(expected) } || finalSubmitTargets(snapshot).size == 1
|
||||
val savedEvidence = waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存超时,未创建订单") { snapshot ->
|
||||
hasSavedAddressEvidence(snapshot, expected, suffix)
|
||||
}
|
||||
if (finalSubmitTargets(saved).size != 1) {
|
||||
if (finalSubmitTargets(savedEvidence).size != 1) {
|
||||
if (!driver.backPurchase()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后无法返回订单页面,未创建订单")
|
||||
saved = waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { finalSubmitTargets(it).size == 1 }
|
||||
waitFor("PURCHASE_ADDRESS_SAVE_TIMEOUT", "地址保存后无法返回订单页面,未创建订单") { finalSubmitTargets(it).size == 1 }
|
||||
}
|
||||
if (saved.nodes.none { it.visible && it.label.contains(expected) }) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "地址保存后回读不一致,未创建订单")
|
||||
return ShippingAddressProof(expected, suffix)
|
||||
}
|
||||
|
||||
private fun hasSavedAddressEvidence(snapshot: UiSnapshot, expected: String, suffix: String): Boolean {
|
||||
if (snapshot.packageName != PDD_PACKAGE || shippingAddressEditors(snapshot).isNotEmpty()) return false
|
||||
val visible = snapshot.nodes.filter { it.visible }
|
||||
val addressContext = visible.count { normalizeAddressText(it.label) == "收货地址" } == 1 ||
|
||||
(mergedDirect(visible.filter { it.enabled && MASKED_PHONE.containsMatchIn(it.label) }).size == 1 &&
|
||||
finalSubmitTargets(snapshot).size == 1)
|
||||
if (!addressContext) return false
|
||||
|
||||
val normalizedExpected = normalizeAddressText(expected)
|
||||
val fullMatches = mergedDirect(visible.filter {
|
||||
normalizeAddressText(it.label).contains(normalizedExpected) && hasExactTaskSuffix(it.label, suffix)
|
||||
})
|
||||
if (fullMatches.size == 1) return true
|
||||
if (fullMatches.size > 1) return false
|
||||
|
||||
val suffixMatches = mergedDirect(visible.filter { hasExactTaskSuffix(it.label, suffix) })
|
||||
return suffixMatches.size == 1
|
||||
}
|
||||
|
||||
private fun normalizeAddressText(value: String): String = value.filterNot(Char::isWhitespace)
|
||||
|
||||
private fun hasExactTaskSuffix(value: String, suffix: String): Boolean =
|
||||
Regex("${Regex.escape(suffix)}(?![0-9])").containsMatchIn(normalizeAddressText(value))
|
||||
|
||||
private fun waitFor(code: String, message: String, predicate: (UiSnapshot) -> Boolean): UiSnapshot {
|
||||
repeat(50) {
|
||||
val snapshot = driver.capture()
|
||||
|
||||
@@ -39,6 +39,52 @@ class PurchaseLiveAutomationTest {
|
||||
assertFalse(driver.clicked.any { it.contains("支付") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `split confirmation address keeps the unique saved task suffix proof`() {
|
||||
val driver = LiveDriver(splitConfirmationAddress = true)
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {})
|
||||
val address = automation.updateShippingAddress("_cg17")
|
||||
|
||||
val final = automation.finalConfirmation(input().copy(addressSuffix = "_cg17"), address)
|
||||
|
||||
assertEquals("_cg17", final.addressSuffix)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `edit field containing suffix cannot impersonate post save evidence`() {
|
||||
val driver = LiveDriver(saveStaysInEdit = true)
|
||||
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg18") }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
|
||||
assertEquals("PURCHASE_ADDRESS_SAVE_TIMEOUT", error.code)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missing confirmation suffix fails before order creation`() {
|
||||
val driver = LiveDriver(hideConfirmationSuffix = true)
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {})
|
||||
val address = automation.updateShippingAddress("_cg19")
|
||||
val error = runCatching { automation.finalConfirmation(input().copy(addressSuffix = "_cg19"), address) }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
|
||||
assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `duplicate independent confirmation suffixes fail before order creation`() {
|
||||
val driver = LiveDriver(splitConfirmationAddress = true, duplicateConfirmationSuffix = true)
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {})
|
||||
val address = automation.updateShippingAddress("_cg20")
|
||||
val error = runCatching { automation.finalConfirmation(input().copy(addressSuffix = "_cg20"), address) }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
|
||||
assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code)
|
||||
assertEquals(0, driver.submitClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `live rule requires reviewed capabilities and payment stays forbidden`() {
|
||||
val live = PurchaseRuleParser.parse(LIVE_RULE)
|
||||
@@ -116,6 +162,10 @@ class PurchaseLiveAutomationTest {
|
||||
private val duplicatePanels: Boolean = false,
|
||||
private val chooserAfterSubmit: Boolean = false,
|
||||
private val trustedChooser: Boolean = true,
|
||||
private val splitConfirmationAddress: Boolean = false,
|
||||
private val hideConfirmationSuffix: Boolean = false,
|
||||
private val duplicateConfirmationSuffix: Boolean = false,
|
||||
private val saveStaysInEdit: Boolean = false,
|
||||
) : PurchaseUiDriver {
|
||||
private var page = "confirmation"
|
||||
private var addressVisible = !addressClipped
|
||||
@@ -162,7 +212,20 @@ class PurchaseLiveAutomationTest {
|
||||
if (duplicatePanels) nodes += node("panel2", "", scrollable = true, bounds = NodeBounds(0, 500, 1080, 2000))
|
||||
if (addressVisible) {
|
||||
nodes += node("phone", "138****5678")
|
||||
nodes += node("address", address)
|
||||
val suffixStart = address.lastIndexOf("_cg")
|
||||
val addressBody = if (suffixStart >= 0) address.substring(0, suffixStart) else address
|
||||
val addressSuffix = if (suffixStart >= 0) address.substring(suffixStart) else ""
|
||||
when {
|
||||
hideConfirmationSuffix -> nodes += node("address", addressBody)
|
||||
splitConfirmationAddress -> {
|
||||
nodes += node("address-body", addressBody, bounds = NodeBounds(20, 700, 900, 780))
|
||||
nodes += node("address-suffix", addressSuffix, bounds = NodeBounds(20, 780, 300, 840))
|
||||
if (duplicateConfirmationSuffix) {
|
||||
nodes += node("address-suffix-2", addressSuffix, bounds = NodeBounds(500, 780, 780, 840))
|
||||
}
|
||||
}
|
||||
else -> nodes += node("address", address)
|
||||
}
|
||||
}
|
||||
snapshot(nodes)
|
||||
}
|
||||
@@ -173,7 +236,7 @@ class PurchaseLiveAutomationTest {
|
||||
when (target.label) {
|
||||
"138****5678" -> page = "panel"
|
||||
"修改" -> page = "edit"
|
||||
"保存" -> page = "panel"
|
||||
"保存" -> if (!saveStaysInEdit) page = "panel"
|
||||
"提交订单" -> { submitClicks++; page = if (chooserAfterSubmit) "chooser" else "order" }
|
||||
}
|
||||
return FreshActionResult.SUCCESS
|
||||
|
||||
Reference in New Issue
Block a user