813 lines
28 KiB
Python
813 lines
28 KiB
Python
"""Payment suite offline judge tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import datetime
|
|
import inspect
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
import pytest
|
|
|
|
from bench_env.task.base import BaseTask
|
|
from bench_env.task.common_tasks import AnswerTask
|
|
from bench_env.task.payment import tasks as payment_tasks
|
|
from bench_env.tests.conftest import make_judge_input
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
DEFAULT_ROUTE = {"app": "launcher", "path": "/"}
|
|
TEST_TS = int(datetime.datetime(2026, 3, 27, 12, 0, 0).timestamp() * 1000)
|
|
BASE_OS_STATE = {
|
|
"time": {"timestamp": TEST_TS},
|
|
"providers": {"sms": {"conversations": [], "messagesByConversationId": {}}},
|
|
"runningApps": [],
|
|
}
|
|
|
|
|
|
def _load_json(*parts: str) -> dict[str, Any]:
|
|
return json.loads(Path(ROOT, *parts).read_text(encoding="utf-8"))
|
|
|
|
|
|
ALIPAY_DEFAULTS = _load_json("apps", "Alipay", "data", "defaults.json")
|
|
WECHAT_DEFAULTS = _load_json("apps", "Wechat", "data", "defaults.json")
|
|
BILIBILI_DEFAULTS = _load_json("apps", "Bilibili", "data", "defaults.json")
|
|
RAILWAY_DEFAULTS = _load_json("apps", "Railway12306", "data", "defaults.json")
|
|
|
|
|
|
ALL_TASK_CLASSES: list[type[BaseTask]] = [
|
|
obj
|
|
for _, obj in inspect.getmembers(payment_tasks, inspect.isclass)
|
|
if issubclass(obj, BaseTask) and obj is not BaseTask and obj.__module__ == payment_tasks.__name__
|
|
]
|
|
ALL_TASK_IDS = [cls.__name__ for cls in ALL_TASK_CLASSES]
|
|
ANSWER_TASK_CLASSES = [cls for cls in ALL_TASK_CLASSES if issubclass(cls, AnswerTask)]
|
|
|
|
|
|
def _copy(value: Any) -> Any:
|
|
return copy.deepcopy(value)
|
|
|
|
|
|
def _base_notes_state() -> dict[str, Any]:
|
|
return {
|
|
"notes": [],
|
|
"todos": [],
|
|
"folders": [{"id": "unfiled", "name": "未分类"}],
|
|
"settings": {"notesViewMode": "grid", "showWordCount": True},
|
|
}
|
|
|
|
|
|
def _today_ymd() -> str:
|
|
return datetime.datetime.fromtimestamp(TEST_TS / 1000).date().isoformat()
|
|
|
|
|
|
def _tomorrow_ymd() -> str:
|
|
return (datetime.datetime.fromtimestamp(TEST_TS / 1000).date() + datetime.timedelta(days=1)).isoformat()
|
|
|
|
|
|
def _make_input(
|
|
*,
|
|
apps_init: dict[str, Any],
|
|
apps_curr: dict[str, Any],
|
|
os_init: dict[str, Any] | None = None,
|
|
os_curr: dict[str, Any] | None = None,
|
|
) -> Any:
|
|
return make_judge_input(
|
|
{"apps": apps_init, "os": _copy(os_init or BASE_OS_STATE)},
|
|
{"apps": apps_curr, "os": _copy(os_curr or os_init or BASE_OS_STATE)},
|
|
route=DEFAULT_ROUTE,
|
|
)
|
|
|
|
|
|
def _append_note(state: dict[str, Any], *, title: str, content: str) -> dict[str, Any]:
|
|
next_state = _copy(state)
|
|
notes = next_state.setdefault("notes", [])
|
|
notes.append(
|
|
{
|
|
"id": f"note_{len(notes) + 1}",
|
|
"title": title,
|
|
"content": content,
|
|
"updatedAt": TEST_TS + len(notes) + 1,
|
|
"folderId": "unfiled",
|
|
}
|
|
)
|
|
return next_state
|
|
|
|
|
|
def _ensure_wechat_chat(state: dict[str, Any], contact_name: str) -> tuple[dict[str, Any], str]:
|
|
contacts = state.setdefault("contacts", [])
|
|
contact = next((item for item in contacts if str(item.get("name") or "") == contact_name), None)
|
|
if contact is None:
|
|
raise ValueError(f"wechat fixture missing contact: {contact_name}")
|
|
wxid = str(contact["wxid"])
|
|
chats = state.setdefault("chats", [])
|
|
chat = next((item for item in chats if str(item.get("id") or "") == wxid), None)
|
|
if chat is None:
|
|
chat = {
|
|
"id": wxid,
|
|
"name": contact_name,
|
|
"contactName": contact_name,
|
|
"user": {"wxid": wxid, "name": contact_name},
|
|
"isMuted": False,
|
|
"isSticky": False,
|
|
"isAlert": False,
|
|
"messages": [],
|
|
}
|
|
chats.append(chat)
|
|
return chat, wxid
|
|
|
|
|
|
def _append_wechat_text(
|
|
state: dict[str, Any],
|
|
*,
|
|
contact_name: str,
|
|
content: str,
|
|
outgoing: bool,
|
|
) -> dict[str, Any]:
|
|
next_state = _copy(state)
|
|
chat, wxid = _ensure_wechat_chat(next_state, contact_name)
|
|
sender = str(next_state.get("user", {}).get("wxid") or "") if outgoing else wxid
|
|
messages = chat.setdefault("messages", [])
|
|
messages.append(
|
|
{
|
|
"id": f"msg_{len(messages) + 1}",
|
|
"type": "text",
|
|
"content": content,
|
|
"senderId": sender,
|
|
"timestamp": TEST_TS + len(messages) + 1,
|
|
}
|
|
)
|
|
return next_state
|
|
|
|
|
|
def _append_alipay_record(
|
|
state: dict[str, Any],
|
|
*,
|
|
record_id: str,
|
|
kind: str,
|
|
delta: float,
|
|
method_id: str,
|
|
counterparty: str,
|
|
timestamp: int,
|
|
transfer_note: str | None = None,
|
|
target_account: str | None = None,
|
|
order_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
next_state = _copy(state)
|
|
record: dict[str, Any] = {
|
|
"id": record_id,
|
|
"counterpartyName": counterparty,
|
|
"displayTitle": counterparty,
|
|
"delta": float(delta),
|
|
"timestamp": int(timestamp),
|
|
"methodId": method_id,
|
|
"kind": kind,
|
|
}
|
|
if transfer_note is not None:
|
|
record["transferNote"] = transfer_note
|
|
if target_account is not None:
|
|
record["targetAccount"] = target_account
|
|
if order_id is not None:
|
|
record["orderId"] = order_id
|
|
next_state.setdefault("transferRecords", [])
|
|
next_state["transferRecords"] = [record, *next_state["transferRecords"]]
|
|
return next_state
|
|
|
|
|
|
def _rail_order(
|
|
*,
|
|
order_id: str,
|
|
status: str,
|
|
date: str,
|
|
from_station: str = "上海",
|
|
to_station: str = "南京",
|
|
train_no: str = "G1001",
|
|
passenger: str = "赵宇轩",
|
|
seat_type: str = "二等座",
|
|
seat_no: str = "06车 15B号",
|
|
price: float = 120.0,
|
|
depart_time: str = "08:00",
|
|
arrive_time: str = "09:25",
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"id": order_id,
|
|
"trainNo": train_no,
|
|
"fromStation": from_station,
|
|
"toStation": to_station,
|
|
"departTime": depart_time,
|
|
"arriveTime": arrive_time,
|
|
"date": date,
|
|
"status": status,
|
|
"createTime": f"{date}T07:00:00.000Z",
|
|
"tickets": [
|
|
{
|
|
"passengerName": passenger,
|
|
"ticketType": "成人票",
|
|
"seatType": seat_type,
|
|
"seatNo": seat_no,
|
|
"price": float(price),
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
class TestTaskDefinitions:
|
|
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
|
|
def test_instantiation(self, cls: type[BaseTask]) -> None:
|
|
task = cls()
|
|
assert task.name == cls.__name__
|
|
assert task.templates
|
|
assert task.apps
|
|
|
|
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
|
|
def test_description_renders(self, cls: type[BaseTask]) -> None:
|
|
task = cls()
|
|
desc = task.description
|
|
assert desc
|
|
assert "{" not in desc
|
|
|
|
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
|
|
def test_task_docstring_present(self, cls: type[BaseTask]) -> None:
|
|
assert cls.__doc__ is not None
|
|
assert cls.__doc__.strip()
|
|
|
|
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
|
|
def test_meta_attrs(self, cls: type[BaseTask]) -> None:
|
|
assert cls.scope in ("S1", "S2", "S3")
|
|
assert cls.objective in ("operate", "query", "hybrid")
|
|
assert cls.composition in ("atomic", "sequential", "transfer", "deep_dive")
|
|
assert cls.difficulty in ("L1", "L2", "L3", "L4")
|
|
|
|
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
|
|
def test_parameters_have_defaults(self, cls: type[BaseTask]) -> None:
|
|
for key, schema in cls.parameters.items():
|
|
if key.startswith("_"):
|
|
continue
|
|
assert "default" in schema
|
|
|
|
@pytest.mark.parametrize("cls", ANSWER_TASK_CLASSES, ids=[c.__name__ for c in ANSWER_TASK_CLASSES])
|
|
def test_answer_task_has_answer(self, cls: type[BaseTask]) -> None:
|
|
# payment suite 当前无 AnswerTask,保留占位以防后续新增遗漏
|
|
pass
|
|
|
|
def test_multichannel_task_removed(self) -> None:
|
|
assert not hasattr(payment_tasks, "AlipayTransferMultiChannel")
|
|
|
|
def test_transfer_with_note_prompt_mentions_password(self) -> None:
|
|
task = payment_tasks.TransferToContactWithNote()
|
|
assert "我的密码是123456" in task.description
|
|
|
|
|
|
def _bind_multiple_cards_positive() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayBindMultipleCardsTransferAndRecordSuccessfulCards()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay["balance"] = {"total": 200.0}
|
|
init_alipay["bankCards"] = [
|
|
{"id": "ccb", "bankName": "建设银行储蓄卡", "cardNumber": "6227000000000005445", "last4": "5445", "bound": True, "available": 20000},
|
|
{"id": "icbc", "bankName": "工商银行储蓄卡", "cardNumber": "6222000000000001234", "last4": "1234", "bound": False, "available": 500},
|
|
{"id": "abc", "bankName": "农业银行储蓄卡", "cardNumber": "6228480000000006789", "last4": "6789", "bound": False, "available": 20000},
|
|
]
|
|
init_alipay["transferRecords"] = []
|
|
init_alipay["lastPaymentHint"] = ""
|
|
curr_alipay = _copy(init_alipay)
|
|
for card in curr_alipay["bankCards"]:
|
|
card["bound"] = True
|
|
target = str(task.p.targetAccount)
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id="bind_tx_1",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount1),
|
|
method_id="ccb",
|
|
counterparty=f"转账-{target}",
|
|
target_account=target,
|
|
transfer_note="第一笔",
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id="bind_tx_2",
|
|
kind="transfer",
|
|
delta=0.0,
|
|
method_id="icbc",
|
|
counterparty=f"转账-{target}",
|
|
target_account=target,
|
|
transfer_note="第二笔工行余额不足",
|
|
timestamp=TEST_TS + 2000,
|
|
)
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id="bind_tx_3",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount3),
|
|
method_id="abc",
|
|
counterparty=f"转账-{target}",
|
|
target_account=target,
|
|
transfer_note="第三笔",
|
|
timestamp=TEST_TS + 3000,
|
|
)
|
|
init_notes = _base_notes_state()
|
|
curr_notes = _append_note(
|
|
init_notes,
|
|
title=str(task.p.noteTitle),
|
|
content="成功银行卡:建设银行储蓄卡(5445)、农业银行储蓄卡(6789)",
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay, "notes": init_notes},
|
|
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
|
|
)
|
|
|
|
|
|
def _continuous_payments_positive() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayContinuousPaymentsToContactsRecordBalances()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay["balance"] = {"total": 2000.0}
|
|
init_alipay["transferRecords"] = []
|
|
curr_alipay = _copy(init_alipay)
|
|
contacts = [
|
|
str(task.p.contact1),
|
|
str(task.p.contact2),
|
|
str(task.p.contact3),
|
|
str(task.p.contact4),
|
|
str(task.p.contact5),
|
|
]
|
|
amounts = [
|
|
float(task.p.amount1),
|
|
float(task.p.amount2),
|
|
float(task.p.amount3),
|
|
float(task.p.amount4),
|
|
float(task.p.amount5),
|
|
]
|
|
running = float(init_alipay["balance"]["total"])
|
|
balances: list[float] = []
|
|
for index, (contact, amount) in enumerate(zip(contacts, amounts, strict=True), start=1):
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id=f"cont_tx_{index}",
|
|
kind="transfer",
|
|
delta=-amount,
|
|
method_id="balance",
|
|
counterparty=contact,
|
|
target_account=contact,
|
|
transfer_note="发工资",
|
|
timestamp=TEST_TS + index * 1000,
|
|
)
|
|
running -= amount
|
|
balances.append(running)
|
|
curr_alipay["balance"]["total"] = balances[-1]
|
|
init_notes = _base_notes_state()
|
|
curr_notes = _append_note(
|
|
init_notes,
|
|
title=str(task.p.noteTitle),
|
|
content="\n".join(f"第{i}次后余额:{balance:g}" for i, balance in enumerate(balances, start=1)),
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay, "notes": init_notes},
|
|
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
|
|
)
|
|
|
|
|
|
def _continuous_payments_negative() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayContinuousPaymentsToContactsRecordBalances()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay["balance"] = {"total": 2000.0}
|
|
init_alipay["transferRecords"] = []
|
|
curr_alipay = _copy(init_alipay)
|
|
contacts = [
|
|
str(task.p.contact1),
|
|
str(task.p.contact2),
|
|
str(task.p.contact3),
|
|
str(task.p.contact4),
|
|
str(task.p.contact5),
|
|
]
|
|
amounts = [
|
|
float(task.p.amount1),
|
|
float(task.p.amount2),
|
|
float(task.p.amount3),
|
|
float(task.p.amount4),
|
|
float(task.p.amount5),
|
|
]
|
|
running = float(init_alipay["balance"]["total"])
|
|
recorded_balances: list[float] = []
|
|
for index, (contact, amount) in enumerate(zip(contacts, amounts, strict=True), start=1):
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id=f"cont_neg_tx_{index}",
|
|
kind="transfer",
|
|
delta=-amount,
|
|
method_id="balance",
|
|
counterparty=contact,
|
|
target_account=contact,
|
|
transfer_note="发工资",
|
|
timestamp=TEST_TS + index * 1000,
|
|
)
|
|
running -= amount
|
|
if index < 5:
|
|
recorded_balances.append(running)
|
|
curr_alipay["balance"]["total"] = running
|
|
init_notes = _base_notes_state()
|
|
curr_notes = _append_note(
|
|
init_notes,
|
|
title=str(task.p.noteTitle),
|
|
content="\n".join(f"第{i}次后余额:{balance:g}" for i, balance in enumerate(recorded_balances, start=1)),
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay, "notes": init_notes},
|
|
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
|
|
)
|
|
|
|
|
|
def _bind_multiple_cards_negative() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayBindMultipleCardsTransferAndRecordSuccessfulCards()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay["balance"] = {"total": 200.0}
|
|
init_alipay["bankCards"] = [
|
|
{"id": "ccb", "bankName": "建设银行储蓄卡", "cardNumber": "6227000000000005445", "last4": "5445", "bound": True, "available": 20000},
|
|
{"id": "icbc", "bankName": "工商银行储蓄卡", "cardNumber": "6222000000000001234", "last4": "1234", "bound": False, "available": 500},
|
|
{"id": "abc", "bankName": "农业银行储蓄卡", "cardNumber": "6228480000000006789", "last4": "6789", "bound": False, "available": 20000},
|
|
]
|
|
init_alipay["transferRecords"] = []
|
|
init_alipay["lastPaymentHint"] = ""
|
|
curr_alipay = _copy(init_alipay)
|
|
for card in curr_alipay["bankCards"]:
|
|
card["bound"] = True
|
|
target = str(task.p.targetAccount)
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id="bind_neg_tx_1",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount1),
|
|
method_id="ccb",
|
|
counterparty=f"转账-{target}",
|
|
target_account=target,
|
|
transfer_note="第一笔",
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id="bind_neg_tx_2",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount2),
|
|
method_id="icbc",
|
|
counterparty=f"转账-{target}",
|
|
target_account=target,
|
|
transfer_note="第二笔",
|
|
timestamp=TEST_TS + 2000,
|
|
)
|
|
init_notes = _base_notes_state()
|
|
curr_notes = _append_note(
|
|
init_notes,
|
|
title=str(task.p.noteTitle),
|
|
content="成功银行卡:建设银行储蓄卡(5445)、工商银行储蓄卡(1234)",
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay, "notes": init_notes},
|
|
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
|
|
)
|
|
|
|
|
|
def _change_password_positive() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayChangePaymentPasswordThenPay()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay.setdefault("userInfo", {})
|
|
init_alipay["userInfo"]["paymentPassword"] = "000000"
|
|
curr_alipay = _copy(init_alipay)
|
|
curr_alipay["userInfo"]["paymentPassword"] = str(task.p.newPassword)
|
|
curr_alipay = _append_alipay_record(
|
|
curr_alipay,
|
|
record_id="pwd_pay_1",
|
|
kind="payment",
|
|
delta=-float(task.p.amount),
|
|
method_id="balance",
|
|
counterparty=str(task.p.contact),
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay},
|
|
apps_curr={"alipay": curr_alipay},
|
|
)
|
|
|
|
|
|
def _change_password_negative() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayChangePaymentPasswordThenPay()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay.setdefault("userInfo", {})
|
|
init_alipay["userInfo"]["paymentPassword"] = "000000"
|
|
curr_alipay = _copy(init_alipay)
|
|
curr_alipay["userInfo"]["paymentPassword"] = str(task.p.newPassword)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay},
|
|
apps_curr={"alipay": curr_alipay},
|
|
)
|
|
|
|
|
|
def _transfer_with_note_positive() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.TransferToContactWithNote()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay["transferRecords"] = []
|
|
curr_alipay = _append_alipay_record(
|
|
init_alipay,
|
|
record_id="transfer_with_note_1",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount),
|
|
method_id="balance",
|
|
counterparty=str(task.p.name),
|
|
target_account=str(task.p.name),
|
|
transfer_note=str(task.p.note),
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
curr_alipay["balance"]["total"] = round(
|
|
float(init_alipay["balance"]["total"]) - float(task.p.amount),
|
|
2,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay},
|
|
apps_curr={"alipay": curr_alipay},
|
|
)
|
|
|
|
|
|
def _transfer_with_note_negative() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.TransferToContactWithNote()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
init_alipay["transferRecords"] = []
|
|
curr_alipay = _append_alipay_record(
|
|
init_alipay,
|
|
record_id="transfer_with_note_wrong_note",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount),
|
|
method_id="balance",
|
|
counterparty=str(task.p.name),
|
|
target_account=str(task.p.name),
|
|
transfer_note="别的备注",
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
curr_alipay["balance"]["total"] = round(
|
|
float(init_alipay["balance"]["total"]) - float(task.p.amount),
|
|
2,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay},
|
|
apps_curr={"alipay": curr_alipay},
|
|
)
|
|
|
|
|
|
def _subscribe_membership_positive() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.SubscribeMembershipAutoRenewThenCancelInWechat()
|
|
init_wechat = _copy(WECHAT_DEFAULTS)
|
|
init_wechat["subscriptions"] = []
|
|
curr_wechat = _copy(init_wechat)
|
|
curr_wechat["subscriptions"] = [
|
|
{
|
|
"id": "sub_task_1",
|
|
"membershipType": str(task.p.membershipType),
|
|
"price": float(task.p.price),
|
|
"billingCycle": str(task.p.billingCycle),
|
|
"autoRenew": False,
|
|
"createdAt": TEST_TS,
|
|
"source": "哔哩哔哩",
|
|
}
|
|
]
|
|
init_bilibili = _copy(BILIBILI_DEFAULTS)
|
|
curr_bilibili = _copy(init_bilibili)
|
|
curr_bilibili["user"]["isVip"] = True
|
|
curr_bilibili["user"]["vipExpireAt"] = TEST_TS + 30 * 24 * 60 * 60 * 1000
|
|
return task, _make_input(
|
|
apps_init={"wechat": init_wechat, "bilibili": init_bilibili},
|
|
apps_curr={"wechat": curr_wechat, "bilibili": curr_bilibili},
|
|
)
|
|
|
|
|
|
def _subscribe_membership_negative() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.SubscribeMembershipAutoRenewThenCancelInWechat()
|
|
init_wechat = _copy(WECHAT_DEFAULTS)
|
|
init_wechat["subscriptions"] = []
|
|
curr_wechat = _copy(init_wechat)
|
|
curr_wechat["subscriptions"] = [
|
|
{
|
|
"id": "sub_task_2",
|
|
"membershipType": str(task.p.membershipType),
|
|
"price": float(task.p.price),
|
|
"billingCycle": str(task.p.billingCycle),
|
|
"autoRenew": True,
|
|
"createdAt": TEST_TS,
|
|
"source": "哔哩哔哩",
|
|
}
|
|
]
|
|
init_bilibili = _copy(BILIBILI_DEFAULTS)
|
|
curr_bilibili = _copy(init_bilibili)
|
|
curr_bilibili["user"]["isVip"] = True
|
|
curr_bilibili["user"]["vipExpireAt"] = TEST_TS + 30 * 24 * 60 * 60 * 1000
|
|
return task, _make_input(
|
|
apps_init={"wechat": init_wechat, "bilibili": init_bilibili},
|
|
apps_curr={"wechat": curr_wechat, "bilibili": curr_bilibili},
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _transfer_notify_positive() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayTransferAndNotify()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
curr_alipay = _append_alipay_record(
|
|
init_alipay,
|
|
record_id="notify_tx_1",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount),
|
|
method_id="balance",
|
|
counterparty=str(task.p.alipay_contact),
|
|
target_account=str(task.p.alipay_contact),
|
|
transfer_note=str(task.p.note),
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
init_wechat = _copy(WECHAT_DEFAULTS)
|
|
curr_wechat = _append_wechat_text(
|
|
init_wechat,
|
|
contact_name=str(task.p.contact),
|
|
content=f"我刚给你转账{float(task.p.amount):g}元,记得查收。",
|
|
outgoing=True,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay, "wechat": init_wechat},
|
|
apps_curr={"alipay": curr_alipay, "wechat": curr_wechat},
|
|
)
|
|
|
|
|
|
def _transfer_notify_negative() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.AlipayTransferAndNotify()
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
curr_alipay = _append_alipay_record(
|
|
init_alipay,
|
|
record_id="notify_tx_neg_1",
|
|
kind="transfer",
|
|
delta=-float(task.p.amount),
|
|
method_id="balance",
|
|
counterparty=str(task.p.alipay_contact),
|
|
target_account=str(task.p.alipay_contact),
|
|
transfer_note=str(task.p.note),
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
init_wechat = _copy(WECHAT_DEFAULTS)
|
|
curr_wechat = _append_wechat_text(
|
|
init_wechat,
|
|
contact_name=str(task.p.contact),
|
|
content="晚点联系你。",
|
|
outgoing=True,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"alipay": init_alipay, "wechat": init_wechat},
|
|
apps_curr={"alipay": curr_alipay, "wechat": curr_wechat},
|
|
)
|
|
|
|
|
|
def _wechat_extract_positive() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.WechatExtractAmountTransfer()
|
|
amount = float(task.p.requestAmount)
|
|
init_wechat = _copy(WECHAT_DEFAULTS)
|
|
curr_wechat = _append_wechat_text(
|
|
init_wechat,
|
|
contact_name=str(task.p.contact),
|
|
content=f"麻烦你转{amount:g}元给我。",
|
|
outgoing=False,
|
|
)
|
|
curr_wechat = _append_wechat_text(
|
|
curr_wechat,
|
|
contact_name=str(task.p.contact),
|
|
content=str(task.p.reply),
|
|
outgoing=True,
|
|
)
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
curr_alipay = _append_alipay_record(
|
|
init_alipay,
|
|
record_id="extract_tx_1",
|
|
kind="transfer",
|
|
delta=-amount,
|
|
method_id="balance",
|
|
counterparty=str(task.p.alipay_contact),
|
|
target_account=str(task.p.alipay_contact),
|
|
transfer_note="按微信请求转账",
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"wechat": init_wechat, "alipay": init_alipay},
|
|
apps_curr={"wechat": curr_wechat, "alipay": curr_alipay},
|
|
)
|
|
|
|
|
|
def _wechat_extract_negative() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.WechatExtractAmountTransfer()
|
|
amount = float(task.p.requestAmount)
|
|
init_wechat = _copy(WECHAT_DEFAULTS)
|
|
curr_wechat = _append_wechat_text(
|
|
init_wechat,
|
|
contact_name=str(task.p.contact),
|
|
content=f"麻烦你转{amount:g}元给我。",
|
|
outgoing=False,
|
|
)
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
curr_alipay = _append_alipay_record(
|
|
init_alipay,
|
|
record_id="extract_tx_neg_1",
|
|
kind="transfer",
|
|
delta=-(amount + 5.0),
|
|
method_id="balance",
|
|
counterparty=str(task.p.alipay_contact),
|
|
target_account=str(task.p.alipay_contact),
|
|
transfer_note="转错金额",
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"wechat": init_wechat, "alipay": init_alipay},
|
|
apps_curr={"wechat": curr_wechat, "alipay": curr_alipay},
|
|
)
|
|
|
|
|
|
def _wechat_extract_missing_reply() -> tuple[BaseTask, Any]:
|
|
task = payment_tasks.WechatExtractAmountTransfer()
|
|
amount = float(task.p.requestAmount)
|
|
init_wechat = _copy(WECHAT_DEFAULTS)
|
|
curr_wechat = _append_wechat_text(
|
|
init_wechat,
|
|
contact_name=str(task.p.contact),
|
|
content=f"麻烦你转{amount:g}元给我。",
|
|
outgoing=False,
|
|
)
|
|
init_alipay = _copy(ALIPAY_DEFAULTS)
|
|
curr_alipay = _append_alipay_record(
|
|
init_alipay,
|
|
record_id="extract_tx_no_reply_1",
|
|
kind="transfer",
|
|
delta=-amount,
|
|
method_id="balance",
|
|
counterparty=str(task.p.alipay_contact),
|
|
target_account=str(task.p.alipay_contact),
|
|
transfer_note="按微信请求转账",
|
|
timestamp=TEST_TS + 1000,
|
|
)
|
|
return task, _make_input(
|
|
apps_init={"wechat": init_wechat, "alipay": init_alipay},
|
|
apps_curr={"wechat": curr_wechat, "alipay": curr_alipay},
|
|
)
|
|
|
|
|
|
OFFLINE_JUDGE_TASK_NAMES = {
|
|
"AlipayContinuousPaymentsToContactsRecordBalances",
|
|
"AlipayBindMultipleCardsTransferAndRecordSuccessfulCards",
|
|
"AlipayChangePaymentPasswordThenPay",
|
|
"SubscribeMembershipAutoRenewThenCancelInWechat",
|
|
"TransferToContactWithNote",
|
|
"AlipayTransferAndNotify",
|
|
"WechatExtractAmountTransfer",
|
|
}
|
|
|
|
|
|
OFFLINE_POSITIVE_CASES: list[tuple[str, Callable[[], tuple[BaseTask, Any]]]] = [
|
|
("AlipayContinuousPaymentsToContactsRecordBalances", _continuous_payments_positive),
|
|
("AlipayBindMultipleCardsTransferAndRecordSuccessfulCards", _bind_multiple_cards_positive),
|
|
("AlipayChangePaymentPasswordThenPay", _change_password_positive),
|
|
("SubscribeMembershipAutoRenewThenCancelInWechat", _subscribe_membership_positive),
|
|
("TransferToContactWithNote", _transfer_with_note_positive),
|
|
("AlipayTransferAndNotify", _transfer_notify_positive),
|
|
("WechatExtractAmountTransfer", _wechat_extract_positive),
|
|
]
|
|
|
|
|
|
OFFLINE_NEGATIVE_CASES: list[tuple[str, Callable[[], tuple[BaseTask, Any]]]] = [
|
|
("AlipayContinuousPaymentsToContactsRecordBalances", _continuous_payments_negative),
|
|
("AlipayBindMultipleCardsTransferAndRecordSuccessfulCards", _bind_multiple_cards_negative),
|
|
("AlipayChangePaymentPasswordThenPay", _change_password_negative),
|
|
("SubscribeMembershipAutoRenewThenCancelInWechat", _subscribe_membership_negative),
|
|
("TransferToContactWithNote", _transfer_with_note_negative),
|
|
("AlipayTransferAndNotify", _transfer_notify_negative),
|
|
("WechatExtractAmountTransfer", _wechat_extract_negative),
|
|
]
|
|
|
|
|
|
class TestTaskJudgeMatrixOffline:
|
|
@pytest.mark.parametrize("name,factory", OFFLINE_POSITIVE_CASES)
|
|
def test_positive(self, name: str, factory: Callable[[], tuple[BaseTask, Any]]) -> None:
|
|
task, inp = factory()
|
|
result = task.evaluate(inp)
|
|
assert result.success, f"positive case {name} failed: {result.issues}"
|
|
|
|
@pytest.mark.parametrize("name,factory", OFFLINE_NEGATIVE_CASES)
|
|
def test_negative(self, name: str, factory: Callable[[], tuple[BaseTask, Any]]) -> None:
|
|
task, inp = factory()
|
|
result = task.evaluate(inp)
|
|
assert not result.success, f"negative case {name} unexpectedly passed"
|
|
|
|
def test_wechat_extract_requires_reply(self) -> None:
|
|
task, inp = _wechat_extract_missing_reply()
|
|
result = task.evaluate(inp)
|
|
assert not result.success
|
|
|
|
def test_matrix_complete(self) -> None:
|
|
pos_names = {name for name, _ in OFFLINE_POSITIVE_CASES}
|
|
neg_names = {name for name, _ in OFFLINE_NEGATIVE_CASES}
|
|
assert pos_names == OFFLINE_JUDGE_TASK_NAMES
|
|
assert neg_names == OFFLINE_JUDGE_TASK_NAMES
|