payment_backend_stub.py (5249B)
1 #!/usr/bin/env python3 2 """Small merchant HTTP stub for payment-backend failure tests.""" 3 4 import json 5 import os 6 import sys 7 import threading 8 import time 9 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer 10 from urllib.parse import urlsplit 11 12 13 TOKEN = "secret-token:stub" 14 TEMPLATE_ID = "premium" 15 TEMPLATE_COUNT = int(os.environ.get("PAIVANA_STUB_TEMPLATE_COUNT", "1")) 16 CONTRACT_PADDING = int(os.environ.get("PAIVANA_STUB_CONTRACT_PADDING", "0")) 17 DETAIL_DELAY = float(os.environ.get("PAIVANA_STUB_DETAIL_DELAY", "0")) 18 BASE_PATH = os.environ.get("PAIVANA_STUB_BASE_PATH", "").rstrip("/") 19 detail_lock = threading.Lock() 20 active_details = 0 21 retry_order_dropped = False 22 retry_order_lock = threading.Lock() 23 24 25 def template_ids(): 26 if TEMPLATE_COUNT == 1: 27 return [TEMPLATE_ID] 28 return [f"{TEMPLATE_ID}-{i:04d}" for i in range(TEMPLATE_COUNT)] 29 30 31 class ReusableServer(ThreadingHTTPServer): 32 allow_reuse_address = True 33 daemon_threads = True 34 35 36 class Handler(BaseHTTPRequestHandler): 37 protocol_version = "HTTP/1.1" 38 39 def log_message(self, fmt, *args): 40 sys.stderr.write("payment_backend_stub: " + fmt % args + "\n") 41 42 def reply(self, status, body): 43 encoded = json.dumps(body, separators=(",", ":")).encode() 44 self.send_response(status) 45 self.send_header("Content-Type", "application/json") 46 self.send_header("Content-Length", str(len(encoded))) 47 self.end_headers() 48 self.wfile.write(encoded) 49 50 def do_GET(self): 51 if self.headers.get("Authorization") != f"Bearer {TOKEN}": 52 self.reply(401, {"code": 2000, "hint": "wrong bearer token"}) 53 return 54 path = urlsplit(self.path).path 55 if BASE_PATH: 56 if not path.startswith(BASE_PATH + "/"): 57 self.reply(404, {"code": 2906, "hint": "wrong base path"}) 58 return 59 path = path[len(BASE_PATH):] 60 if path == "/private/templates": 61 self.reply( 62 200, 63 { 64 "templates": [ 65 { 66 "template_id": template_id, 67 "template_description": "Paywalled content", 68 } 69 for template_id in template_ids() 70 ] 71 }, 72 ) 73 return 74 if path.removeprefix("/private/templates/") in template_ids(): 75 global active_details 76 with detail_lock: 77 active_details += 1 78 current = active_details 79 print(f"template detail concurrency {current}", flush=True) 80 try: 81 if DETAIL_DELAY: 82 time.sleep(DETAIL_DELAY) 83 self.reply( 84 200, 85 { 86 "template_description": "Paywalled content", 87 "template_contract": { 88 "template_type": "paivana", 89 "summary": "Access to the article" + "x" * CONTRACT_PADDING, 90 "website_regex": ".*", 91 "max_pickup_duration": {"d_us": 3600000000}, 92 "choices": [ 93 {"amount": "TESTKUDOS:1", "description": "One article"} 94 ], 95 }, 96 }, 97 ) 98 finally: 99 with detail_lock: 100 active_details -= 1 101 return 102 if path == "/private/orders/timeout-order": 103 # Longer than Paivana's five-second order deadline. The 104 # client will close the connection before this reply. 105 time.sleep(10) 106 self.reply(404, {"code": 2906, "hint": "late test reply"}) 107 return 108 if path == "/private/orders/retry-order": 109 global retry_order_dropped 110 with retry_order_lock: 111 drop = not retry_order_dropped 112 retry_order_dropped = True 113 if drop: 114 # Simulate a pooled connection that the backend closed while 115 # it was idle: the request reached the server, but no HTTP 116 # response reached Paivana. The idempotent lookup should be 117 # retried once on another connection. 118 print("dropping first /private/orders/retry-order request", 119 flush=True) 120 self.close_connection = True 121 self.connection.shutdown(2) 122 self.connection.close() 123 return 124 self.reply(404, {"code": 2906, "hint": "unknown test order"}) 125 return 126 if path.startswith("/private/orders/"): 127 self.reply(404, {"code": 2906, "hint": "unknown test order"}) 128 return 129 self.reply(404, {"code": 2906, "hint": "unknown test endpoint"}) 130 131 132 def main(): 133 if len(sys.argv) != 2: 134 raise SystemExit(f"usage: {sys.argv[0]} PORT") 135 server = ReusableServer(("127.0.0.1", int(sys.argv[1])), Handler) 136 print(f"payment_backend_stub listening on {server.server_port}", flush=True) 137 server.serve_forever() 138 139 140 if __name__ == "__main__": 141 main()