Files
ske087 bff6d70b17 Initial commit: aplicatie de cantarire si etichetare
Aplicatie Flask + SQLite pentru productie alimentara: cantarirea
componentelor unui produs finit si tiparirea etichetei prin QZ Tray.

- produse finite (retete) formate din componente cantarite
- designer vizual de etichete (mm/px, zona sigura, campuri variabile,
  cod QR cu continut din mai multe bucati)
- legare produs finit -> sablon de eticheta
- navigare pe doua taburi: Cantarire / Setari
- administrare: componente, produse finite, sabloane, campuri, istoric
- backup, resetare baza de date, checklist de pregatire, /healthz
- CLI de administrare si serviciu systemd pentru productie
- vendor/syncpulse-scale-bridge (GPL-3.0-or-later) pentru citirea cantarului
2026-10-18 23:38:45 +03:00

157 lines
6.5 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
"""The HTTP interface, end to end, with simulated scales."""
import json
import threading
import time
import urllib.error
import urllib.request
import pytest
from scale_bridge.config import Config
from scale_bridge.detect import Timings
from scale_bridge.server import Bridge, make_server
LOCAL = {"X-Requested-With": "ScaleBridge"}
@pytest.fixture(scope="module")
def bridge_url(tmp_path_factory):
config = Config(str(tmp_path_factory.mktemp("bridge") / "bridge.json"))
bridge = Bridge(config, simulate="mahak_mds,toledo_cont:4800:7E1")
bridge.detect_timings = Timings(passive_s=0.5, probe_s=0.15, confirm_s=0.4, drain_s=0.02)
server = make_server(bridge, "127.0.0.1", 0)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
yield f"http://127.0.0.1:{server.server_address[1]}", config
server.shutdown()
bridge.registry.stop_all()
def call(url, method="GET", body=None, headers=None):
req = urllib.request.Request(url, method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Content-Type": "application/json", **(headers or {})})
try:
with urllib.request.urlopen(req, timeout=15) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read() or b"{}")
def test_health_is_open_and_identifies_the_author(bridge_url):
url, _ = bridge_url
code, body = call(url + "/health")
assert code == 200 and body["service"] == "syncpulse-scale-bridge"
assert body["author"] == "Farshad Zamani" and "weight" in body["capabilities"]
def test_key_is_required(bridge_url):
url, config = bridge_url
assert call(url + "/ports")[0] == 401
assert call(url + "/ports", headers={"X-Bridge-Token": "wrong"})[0] == 401
code, body = call(url + "/ports", headers={"X-Bridge-Token": config.token})
assert code == 200 and any(p["device"] == "COM90" for p in body["ports"])
def test_legacy_read_contract(bridge_url):
url, config = bridge_url
code, body = call(url + "/read", "POST", {
"serial": {"port": "COM90", "baudrate": 9600, "bytesize": 8, "parity": "N", "stopbits": 1},
"exchange": {"request_hex": "4D 09 04 07", "poll_interval_ms": 300}, "frames": 3},
headers={"X-Bridge-Token": config.token})
assert code == 200 and body["ok"] and body["open"]
assert body["frames"] and body["frames"][-1]["hex"].startswith("58")
assert {"t", "hex"} <= set(body["frames"][-1]) and "now" in body
def test_weight_by_port_and_protocol(bridge_url):
url, config = bridge_url
code, body = call(url + "/weight?port=COM91&protocol=toledo_cont&baud=4800&framing=7E1",
headers={"X-Bridge-Token": config.token})
assert code == 200 and body["ok"] and body["unit"] == "kg" and body["checksum_ok"] is True
assert isinstance(body["value"], float) and "measured_at" in body
def test_weight_errors(bridge_url):
url, config = bridge_url
h = {"X-Bridge-Token": config.token}
assert call(url + "/weight?profile=missing", headers=h)[0] == 404
assert call(url + "/weight?port=COM90", headers=h)[0] == 400
assert call(url + "/weight?port=COM90&protocol=mt_sics&framing=9X9", headers=h)[0] == 400
def test_local_api_needs_header_and_local_host(bridge_url):
url, _ = bridge_url
assert call(url + "/api/state")[0] == 403
assert call(url + "/api/state", headers={**LOCAL, "Host": "evil.example"})[0] == 403
code, state = call(url + "/api/state", headers=LOCAL)
assert code == 200 and state["about"]["author"] == "Farshad Zamani" and state["token"]
def test_page_is_served_with_signature(bridge_url):
url, _ = bridge_url
with urllib.request.urlopen(url + "/", timeout=5) as r:
html = r.read().decode("utf-8")
assert "Content-Security-Policy" in r.headers
assert "Farshad Zamani" in html and "linkedin.com/in/farshad-zamani92" in html
assert "cloudtart.com" in html and "SyncPulse" in html
def test_detect_save_read_delete(bridge_url):
url, config = bridge_url
code, body = call(url + "/api/detect", "POST", {"ports": ["COM90"]}, headers=LOCAL)
assert code == 200 and body["job"]["running"]
# a second search while the first runs is refused, and the port is reserved
assert call(url + "/api/detect", "POST", {"ports": ["COM90"]}, headers=LOCAL)[0] == 409
busy = call(url + "/weight?port=COM90&protocol=mahak_mds", headers={"X-Bridge-Token": config.token})
assert busy[0] == 409
deadline = time.time() + 60
job = None
while time.time() < deadline:
job = call(url + "/api/detect", headers=LOCAL)[1]["job"]
if not job["running"]:
break
time.sleep(0.3)
assert job and job["results"][0]["found"] and job["results"][0]["protocol"] == "mahak_mds"
assert job["progress"] == 1.0
code, saved = call(url + "/api/profiles", "POST",
{"detection": job["results"][0], "name": "packing-1", "label": "Packing"},
headers=LOCAL)
assert code == 200 and saved["profile"]["name"] == "packing-1"
code, listed = call(url + "/profiles", headers={"X-Bridge-Token": config.token})
assert [p["name"] for p in listed["profiles"]] == ["packing-1"]
deadline = time.time() + 10
body = {}
while time.time() < deadline:
body = call(url + "/weight?profile=packing-1", headers={"X-Bridge-Token": config.token})[1]
if body.get("stable"):
break
time.sleep(0.4)
assert body["ok"] and body["unit"] == "g" and body["stable"] is True
code, _ = call(url + "/api/profiles/packing-1", "DELETE", headers=LOCAL)
assert code == 200 and config.profiles() == []
def test_allowed_clients_do_not_lock_out_this_pc(bridge_url):
url, config = bridge_url
code, _ = call(url + "/api/allowed", "POST", {"clients": ["10.1.2.3"]}, headers=LOCAL)
assert code == 200
try:
assert call(url + "/ports", headers={"X-Bridge-Token": config.token})[0] == 200
finally:
call(url + "/api/allowed", "POST", {"clients": []}, headers=LOCAL)
def test_bad_json_is_a_400(bridge_url):
url, config = bridge_url
req = urllib.request.Request(url + "/read", method="POST", data=b"{nope",
headers={"X-Bridge-Token": config.token})
with pytest.raises(urllib.error.HTTPError) as e:
urllib.request.urlopen(req, timeout=5)
assert e.value.code == 400