#!/usr/bin/env python3 """Kiểm tra API nhận biển số — đóng vai camera TingPush, gửi đúng như camera gửi. Chỉ cần Python 3, không cài thêm gì. python kiem_tra_api.py http://localhost:5000/api/plates python kiem_tra_api.py https://may-chu.vn/api/plates --header "Authorization: Bearer abc123" python kiem_tra_api.py https://may-chu.vn/api/plates --header "X-Api-Key: abc123" --insecure Các bước: bản "Gửi thử", bản ghi có ảnh toàn cảnh + ảnh biển số, gửi lại đúng bản đó (trùng), sai token, ảnh lớn ~3 MB (như ảnh luồng chính 4K), thời gian trả lời. Không bước nào xóa dữ liệu của bạn — nhưng bước 2 và 5 tạo 2 bản ghi biển 99Z-999.99 (camera_id LPR-KIEMTRA00000) để bạn xóa sau. """ import argparse import base64 import json import ssl import sys import time import urllib.error import urllib.request # ảnh JPEG 176x120 thật (ảnh biển số mẫu) JPEG = base64.b64decode("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") ap = argparse.ArgumentParser() ap.add_argument("url") ap.add_argument("--header", default="", help='header xác thực, VD "Authorization: Bearer abc123"') ap.add_argument("--timeout", type=float, default=8, help="giống 'Thời gian chờ máy nhận' trên app (mặc định 8 giây)") ap.add_argument("--insecure", action="store_true", help="bỏ kiểm tra chứng chỉ HTTPS (camera mặc định cũng bỏ)") a = ap.parse_args() ctx = ssl._create_unverified_context() if a.insecure else None hname, _, hval = a.header.partition(":") ok_all = True def post(body, header=True): data = json.dumps(body, ensure_ascii=False).encode() req = urllib.request.Request(a.url, data=data, method="POST", headers={"Content-Type": "application/json; charset=utf-8", "User-Agent": "lpr-camera/1.0", "Connection": "close"}) if header and a.header: req.add_header(hname.strip(), hval.strip()) t = time.time() try: with urllib.request.urlopen(req, timeout=a.timeout, context=ctx) as r: return r.status, r.read(500).decode("utf-8", "replace"), time.time() - t except urllib.error.HTTPError as e: return e.code, e.read(500).decode("utf-8", "replace"), time.time() - t except Exception as e: # mất kết nối, quá giờ, DNS, TLS... return 0, f"{type(e).__name__}: {e}", time.time() - t def check(name, cond, status, text, secs, hint=""): global ok_all ok_all &= cond print(f"[{'ĐẠT' if cond else 'LỖI'}] {name}: HTTP {status or '—'} sau {secs:.2f}s {text[:160]!r}") if not cond and hint: print(f" -> {hint}") now = int(time.time()) ev = { "event_id": f"LPR-KIEMTRA00000-{now:010d}-000001", "device_id": "KIEM-TRA", "camera_id": "LPR-KIEMTRA00000", "plate": "99Z99999", "plate_display": "99Z-999.99", "raw_text": "99Z-99999", "confidence": 0.9, "read_count": 3, "time": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(now)) + time.strftime("%z")[:3] + ":" + time.strftime("%z")[3:], "timestamp": now, "box": [1520, 980, 1810, 1105], "frame_width": 3840, "frame_height": 2160, } print(f"Kiểm tra {a.url}\n") s, t, d = post({**ev, "event_id": f"LPR-KIEMTRA00000-test-{now:010d}", "raw_text": "TEST", "test": True}) check("1. Bản 'Gửi thử' (test: true)", 200 <= s < 300, s, t, d, "phải trả 2xx; nên bỏ qua không lưu bản có \"test\": true") img = {"image_type": "jpeg", "image": base64.b64encode(JPEG).decode(), "plate_image_type": "jpeg", "plate_image": base64.b64encode(JPEG).decode()} s, t, d = post({**ev, **img}) check("2. Bản ghi có ảnh toàn cảnh + ảnh biển số", 200 <= s < 300, s, t, d, "2xx = đã nhận. 4xx khác 408/429 -> camera BỎ bản ghi; 5xx / mất kết nối -> camera gửi lại") s, t, d = post({**ev, **img}) check("3. Gửi lại đúng bản đó (cùng event_id)", 200 <= s < 300, s, t, d, "bản trùng phải trả 2xx (và không lưu thêm), nếu không camera hiểu là lỗi") if a.header: s, t, d = post(ev, header=False) check("4. Thiếu header xác thực", s in (401, 403), s, t, d, "không có / sai token nên trả 401 hoặc 403") else: print("[BỎ QUA] 4. Thiếu header xác thực — chạy với --header để kiểm tra") big = JPEG + b"\0" * (2_200_000) # ~3 MB sau base64, như ảnh luồng chính 4K s, t, d = post({**ev, "event_id": ev["event_id"][:-1] + "2", "image_type": "jpeg", "image": base64.b64encode(big).decode()}) check("5. Ảnh lớn ~3 MB", 200 <= s < 300, s, t, d, "413 = giới hạn dung lượng: tăng client_max_body_size (nginx), post_max_size (PHP), MaxRequestBodySize (.NET)") slow = d > a.timeout * 0.6 print(f"\n{'Trả lời hơi chậm' if slow else 'Thời gian trả lời ổn'}: chậm nhất nên dưới {a.timeout * 0.6:.1f}s " f"(camera chờ {a.timeout:.0f}s rồi coi là lỗi và gửi lại).") print("\nKẾT QUẢ:", "ĐẠT — API sẵn sàng nhận dữ liệu từ camera" if ok_all else "CHƯA ĐẠT — xem các dòng LỖI ở trên") sys.exit(0 if ok_all else 1)