Skip to content

Commit cc3a709

Browse files
heehoclaude
andcommitted
test(submit_runtime): P1-05 broker create mock + auto_verify 단위 테스트 5건
`_fetch_many` mock 패턴으로 P0-02 capture 응답 시뮬레이션 추가: - classify_broker_reject 7개 status_code 분기 enum 매핑 검증 - place_order success path: orderId 존재 시 submit_state=submitted + broker_ack 의 broker_order_id/order_no/order_date/is_reserved/http_status 6필드 보존 (P0-02 SCHD capture 데이터: V56qyv7r/orderNo=3/2026-05-04) - place_order missing orderId: 200 OK 인데 orderId 부재 → broker_rejected + BROKER_REJECTED_UNKNOWN - place_order RuntimeError timeout: _fetch_many raise → broker_rejected + BROKER_REJECTED_TIMEOUT + http_status=0 - auto_verify=True + submitted: runtime.verify_order 1회 호출 + verify_snapshot/verification_state 가 place_order 응답.data 에 합쳐짐 helper 신규: _enable_final_submit (env 두 개 set), _stub_place_order_dependencies (order_preview + _run_prepare_preflight mock + MUTATION_JOURNAL_FILE patch). test_submit_runtime.py 17 PASS (12→17). 전체 스위트 70 PASS / 1 skipped / 1 무관 fail (bridge_lib.PORT 환경 분리, P1-01부터 일관). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 17a95a3 commit cc3a709

1 file changed

Lines changed: 236 additions & 0 deletions

File tree

tests/test_submit_runtime.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
build_order_preview_receipt,
1717
find_recent_mutation_by_id,
1818
)
19+
from toss_browser_bridge.daemon import classify_broker_reject
1920

2021

2122
class FakePage:
@@ -558,3 +559,238 @@ def test_final_submit_can_be_explicitly_unblocked_for_manual_non_pytest_runtime(
558559

559560
assert runtime._final_submit_enabled is True
560561
assert runtime._mutation_runtime_state()["final_submit_guard_reason"] == "enabled_by_env"
562+
563+
564+
def test_classify_broker_reject_maps_status_codes_to_enum() -> None:
565+
assert classify_broker_reject(message=None, status_code=0, error="EAI_AGAIN") == "BROKER_REJECTED_TIMEOUT"
566+
assert classify_broker_reject(message="gateway timeout", status_code=504, error=None) == "BROKER_REJECTED_TIMEOUT"
567+
assert classify_broker_reject(message="forbidden", status_code=403, error=None) == "BROKER_REJECTED_AUTH_REQUIRED"
568+
assert classify_broker_reject(message="unauthorized", status_code=401, error=None) == "BROKER_REJECTED_AUTH_REQUIRED"
569+
assert classify_broker_reject(message="server error", status_code=500, error=None) == "BROKER_REJECTED_HTTP_ERROR"
570+
assert classify_broker_reject(message="unknown", status_code=200, error=None) == "BROKER_REJECTED_UNKNOWN"
571+
assert classify_broker_reject(message=None, status_code=400, error=None) == "BROKER_REJECTED_UNKNOWN"
572+
573+
574+
def _enable_final_submit(monkeypatch) -> None:
575+
monkeypatch.setenv("TOSS_BRIDGE_ENABLE_FINAL_SUBMIT", "1")
576+
monkeypatch.setenv("TOSS_BRIDGE_ALLOW_TEST_FINAL_SUBMIT", "1")
577+
578+
579+
def _stub_place_order_dependencies(runtime, monkeypatch, tmp_path, fingerprint) -> None:
580+
monkeypatch.setattr(daemon_module, "MUTATION_JOURNAL_FILE", tmp_path / "mutation-journal.jsonl")
581+
runtime.order_preview = MethodType(
582+
lambda self, params: {
583+
"ok": True,
584+
"kind": "order_preview",
585+
"checked_at": "2026-05-04T23:40:00+09:00",
586+
"diagnostics": {"endpoint_matrix": [], "last_errors": []},
587+
"data": {
588+
"preview_state": "preview_ready",
589+
"preview_fingerprint": fingerprint,
590+
},
591+
},
592+
runtime,
593+
)
594+
runtime._run_prepare_preflight = MethodType(
595+
lambda self, normalized: {
596+
"message": "prepare preflight succeeded",
597+
"context": self._make_context([]),
598+
"account_no": "44258118-01",
599+
"submit_market": "NSQ",
600+
"currency_mode": "USD",
601+
"allow_auto_exchange": False,
602+
"prepare_payload": {
603+
"stockCode": "US0378331005",
604+
"tradeType": "buy",
605+
"market": "NSQ",
606+
"currencyMode": "USD",
607+
"price": 200.0,
608+
"quantity": 1,
609+
"orderAmount": 0,
610+
"orderPriceType": "00",
611+
"agreedOver100Million": False,
612+
"marginTrading": False,
613+
"max": False,
614+
"isReservationOrder": False,
615+
"openPriceSinglePriceYn": False,
616+
"withOrderKey": True,
617+
},
618+
"prepare_body": {"orderKey": "trade::session::stub"},
619+
"prepared_order_info": {"price": 200.0, "quantity": 1},
620+
},
621+
runtime,
622+
)
623+
624+
625+
def test_place_order_submits_when_broker_create_returns_order_id(tmp_path, monkeypatch) -> None:
626+
_enable_final_submit(monkeypatch)
627+
runtime = _runtime()
628+
receipt = _receipt()
629+
_stub_place_order_dependencies(runtime, monkeypatch, tmp_path, receipt["preview_fingerprint"])
630+
runtime._fetch_many = MethodType(
631+
lambda self, requests: [
632+
{
633+
"name": "order_create",
634+
"method": "POST",
635+
"path": "/api/v2/wts/trading/order/create",
636+
"status_code": 200,
637+
"ok": True,
638+
"json": {
639+
"result": {
640+
"message": "AAPL 매수 주문 완료",
641+
"orderDate": "2026-05-04",
642+
"orderNo": 3,
643+
"isReserved": False,
644+
"orderId": "V56qyv7r",
645+
}
646+
},
647+
"text": "",
648+
"error": None,
649+
}
650+
],
651+
runtime,
652+
)
653+
654+
response = runtime.place_order(
655+
{
656+
"preview_receipt": receipt,
657+
"preview_fingerprint": receipt["preview_fingerprint"],
658+
"confirm": True,
659+
"confirm_text": "BUY 1 AAPL LIMIT 200.00 US",
660+
}
661+
)
662+
663+
assert response["ok"] is True
664+
data = response["data"]
665+
assert data["submit_state"] == "submitted"
666+
assert data["broker_ack"]["status"] == "submitted"
667+
assert data["broker_ack"]["code"] == "OK"
668+
assert data["broker_ack"]["broker_order_id"] == "V56qyv7r"
669+
assert data["broker_ack"]["order_no"] == 3
670+
assert data["broker_ack"]["order_date"] == "2026-05-04"
671+
assert data["broker_ack"]["http_status"] == 200
672+
673+
674+
def test_place_order_marks_broker_rejected_when_orderid_missing(tmp_path, monkeypatch) -> None:
675+
_enable_final_submit(monkeypatch)
676+
runtime = _runtime()
677+
receipt = _receipt()
678+
_stub_place_order_dependencies(runtime, monkeypatch, tmp_path, receipt["preview_fingerprint"])
679+
runtime._fetch_many = MethodType(
680+
lambda self, requests: [
681+
{
682+
"name": "order_create",
683+
"method": "POST",
684+
"path": "/api/v2/wts/trading/order/create",
685+
"status_code": 200,
686+
"ok": True,
687+
"json": {"result": {"message": "주문 거절"}},
688+
"text": "",
689+
"error": None,
690+
}
691+
],
692+
runtime,
693+
)
694+
695+
response = runtime.place_order(
696+
{
697+
"preview_receipt": receipt,
698+
"preview_fingerprint": receipt["preview_fingerprint"],
699+
"confirm": True,
700+
"confirm_text": "BUY 1 AAPL LIMIT 200.00 US",
701+
}
702+
)
703+
704+
data = response["data"]
705+
assert data["submit_state"] == "broker_rejected"
706+
assert data["broker_ack"]["code"] == "BROKER_REJECTED_UNKNOWN"
707+
assert "broker_order_id" not in data["broker_ack"]
708+
709+
710+
def test_place_order_marks_broker_rejected_with_timeout_on_runtime_error(tmp_path, monkeypatch) -> None:
711+
_enable_final_submit(monkeypatch)
712+
runtime = _runtime()
713+
receipt = _receipt()
714+
_stub_place_order_dependencies(runtime, monkeypatch, tmp_path, receipt["preview_fingerprint"])
715+
716+
def _raise_timeout(self, requests):
717+
raise RuntimeError("browser request timed out: 30000ms")
718+
719+
runtime._fetch_many = MethodType(_raise_timeout, runtime)
720+
721+
response = runtime.place_order(
722+
{
723+
"preview_receipt": receipt,
724+
"preview_fingerprint": receipt["preview_fingerprint"],
725+
"confirm": True,
726+
"confirm_text": "BUY 1 AAPL LIMIT 200.00 US",
727+
}
728+
)
729+
730+
data = response["data"]
731+
assert data["submit_state"] == "broker_rejected"
732+
assert data["broker_ack"]["code"] == "BROKER_REJECTED_TIMEOUT"
733+
assert data["broker_ack"]["http_status"] == 0
734+
735+
736+
def test_place_order_auto_verify_attaches_verify_snapshot_when_submitted(tmp_path, monkeypatch) -> None:
737+
_enable_final_submit(monkeypatch)
738+
runtime = _runtime()
739+
receipt = _receipt()
740+
_stub_place_order_dependencies(runtime, monkeypatch, tmp_path, receipt["preview_fingerprint"])
741+
runtime._fetch_many = MethodType(
742+
lambda self, requests: [
743+
{
744+
"name": "order_create",
745+
"method": "POST",
746+
"path": "/api/v2/wts/trading/order/create",
747+
"status_code": 200,
748+
"ok": True,
749+
"json": {
750+
"result": {
751+
"message": "AAPL 매수 주문 완료",
752+
"orderDate": "2026-05-04",
753+
"orderNo": 3,
754+
"isReserved": False,
755+
"orderId": "V56qyv7r",
756+
}
757+
},
758+
"text": "",
759+
"error": None,
760+
}
761+
],
762+
runtime,
763+
)
764+
verify_calls: list[dict] = []
765+
766+
def _fake_verify_order(self, params):
767+
verify_calls.append(params)
768+
return {
769+
"ok": True,
770+
"kind": "verify_order",
771+
"data": {
772+
"verification_state": "verified_success",
773+
"verify_snapshot": {
774+
"status": "verified_success",
775+
"matched_order": {"order_no": 3},
776+
"verified_at": "2026-05-04T23:41:00+09:00",
777+
},
778+
},
779+
}
780+
781+
runtime.verify_order = MethodType(_fake_verify_order, runtime)
782+
783+
response = runtime.place_order(
784+
{
785+
"preview_receipt": receipt,
786+
"preview_fingerprint": receipt["preview_fingerprint"],
787+
"confirm": True,
788+
"confirm_text": "BUY 1 AAPL LIMIT 200.00 US",
789+
"auto_verify": True,
790+
}
791+
)
792+
793+
data = response["data"]
794+
assert len(verify_calls) == 1
795+
assert data["verification_state"] == "verified_success"
796+
assert data["verify_snapshot"]["matched_order"]["order_no"] == 3

0 commit comments

Comments
 (0)