test: isolate fee void ACK loss and preserve wallet delivery
This commit is contained in:
@@ -32,12 +32,15 @@ def encoded(value):
|
||||
|
||||
|
||||
class State:
|
||||
def __init__(self, event_type, event_id, loading_plan_id, log_path=None, container_booking_id=None, delivery_id=None):
|
||||
def __init__(self, event_type, event_id, loading_plan_id, log_path=None, container_booking_id=None, delivery_id=None,
|
||||
fee_source_id=None, source_version=None):
|
||||
self.event_type = event_type
|
||||
self.selector_id = event_id
|
||||
self.plan_id = loading_plan_id
|
||||
self.booking_id = container_booking_id
|
||||
self.delivery_id = delivery_id
|
||||
self.fee_source_id = fee_source_id
|
||||
self.source_version = source_version
|
||||
self.log_path = log_path
|
||||
self.lock = threading.Lock()
|
||||
self.mode = "PASS"
|
||||
@@ -73,6 +76,9 @@ class State:
|
||||
selected = isinstance(payload, dict) and str(payload.get("sourceContainerBookingId")) == self.booking_id
|
||||
elif self.delivery_id:
|
||||
selected = isinstance(payload, dict) and str(payload.get("deliveryOrderId")) == self.delivery_id
|
||||
elif self.fee_source_id:
|
||||
selected = (isinstance(payload, dict) and str(payload.get("sourceFeeId")) == self.fee_source_id
|
||||
and str(payload.get("sourceVersion")) == self.source_version and payload.get("action") == "VOIDED")
|
||||
else:
|
||||
selected = isinstance(payload, dict) and str(payload.get("loadingPlanId")) == self.plan_id
|
||||
qualifies = event.get("eventType") == self.event_type and selected
|
||||
@@ -146,16 +152,22 @@ class QuietHandler(BaseHTTPRequestHandler):
|
||||
def data_handler(state, upstream, timeout):
|
||||
parts = urllib.parse.urlsplit(upstream)
|
||||
endpoint = urllib.parse.urlunsplit(("", "", parts.path or "/", parts.query, ""))
|
||||
# Both consumers share FINANCE_SERVICE_URL. Preserve wallet delivery while
|
||||
# targeting one fee event; never select wallet bodies for fault injection.
|
||||
wallet_endpoint = "/internal/finance/v1/wallet-command-events"
|
||||
wallet_passthrough = (state.fee_source_id is not None
|
||||
and endpoint == "/internal/finance/v1/business-fee-source-events")
|
||||
|
||||
class DataHandler(QuietHandler):
|
||||
def do_POST(self):
|
||||
if self.path != endpoint:
|
||||
passthrough = wallet_passthrough and self.path == wallet_endpoint
|
||||
if self.path != endpoint and not passthrough:
|
||||
self.safe_error(404, "UNEXPECTED_EVENT_ENDPOINT")
|
||||
return
|
||||
body = self.read_body()
|
||||
if body is None:
|
||||
return
|
||||
identity, matched, error_code, attempt, mode = state.select(body)
|
||||
identity, matched, error_code, attempt, mode = (None, False, None, 0, "PASS") if passthrough else state.select(body)
|
||||
if error_code:
|
||||
state.record(identity, matched, attempt, None, error_code)
|
||||
self.safe_error(409, error_code)
|
||||
@@ -164,7 +176,8 @@ def data_handler(state, upstream, timeout):
|
||||
connection_headers = set(self.headers.get("Connection", "").lower().replace(" ", "").split(","))
|
||||
headers = {name: value for name, value in self.headers.items()
|
||||
if name.lower() not in HOP_HEADERS | connection_headers}
|
||||
request = urllib.request.Request(upstream, data=body, headers=headers, method="POST")
|
||||
destination = urllib.parse.urlunsplit((parts.scheme, parts.netloc, wallet_endpoint, "", "")) if passthrough else upstream
|
||||
request = urllib.request.Request(destination, data=body, headers=headers, method="POST")
|
||||
try:
|
||||
# Redirects are forbidden: a redirect could expose a service identity token.
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
@@ -264,6 +277,8 @@ def main():
|
||||
selector.add_argument("--loading-plan-id")
|
||||
selector.add_argument("--container-booking-id")
|
||||
selector.add_argument("--delivery-id")
|
||||
selector.add_argument("--fee-source-id")
|
||||
parser.add_argument("--source-version")
|
||||
parser.add_argument("--bind", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=0)
|
||||
parser.add_argument("--control-port", type=int, default=0)
|
||||
@@ -284,9 +299,17 @@ def main():
|
||||
parser.error("container-booking-id must be a positive decimal identifier")
|
||||
if args.delivery_id and not re.fullmatch(r"[1-9][0-9]{0,19}", args.delivery_id):
|
||||
parser.error("delivery-id must be a positive decimal identifier")
|
||||
if bool(args.fee_source_id) != bool(args.source_version):
|
||||
parser.error("fee-source-id and source-version are required together")
|
||||
if args.fee_source_id and (not re.fullmatch(r"[1-9][0-9]{0,19}", args.fee_source_id)
|
||||
or not re.fullmatch(r"[1-9][0-9]{0,9}", args.source_version)
|
||||
or int(args.source_version) > 2147483647
|
||||
or args.event_type != "BusinessFeeSourceChanged"):
|
||||
parser.error("fee void requires valid source identity, version and BusinessFeeSourceChanged")
|
||||
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9]{0,79}", args.event_type) or not 0 < args.timeout <= 120:
|
||||
parser.error("invalid event type or timeout")
|
||||
state = State(args.event_type, args.event_id, args.loading_plan_id, args.log, args.container_booking_id, args.delivery_id)
|
||||
state = State(args.event_type, args.event_id, args.loading_plan_id, args.log, args.container_booking_id, args.delivery_id,
|
||||
args.fee_source_id, args.source_version)
|
||||
data = ThreadingHTTPServer((args.bind, args.port), data_handler(state, args.upstream, args.timeout))
|
||||
control = ThreadingHTTPServer(("127.0.0.1", args.control_port), control_handler(state, token))
|
||||
for server in (data, control):
|
||||
|
||||
Reference in New Issue
Block a user