import json import httpx import pytest from app.cc2control import CC2Control, CC2Error, clean_text, encode_body, spoolman_id_of, tray_statuses from app.spoolman import Spoolman class Recorder: def __init__(self, status=200, body=None): self.requests: list[httpx.Request] = [] self.status = status self.body = body if body is not None else {"ok": True} def __call__(self, request: httpx.Request): self.requests.append(request) return httpx.Response(self.status, json=self.body) def client(rec): return CC2Control("http://10.10.2.202:8081", transport=httpx.MockTransport(rec)) def test_write_request_format(): rec = Recorder(201, {"ok": True, "id": "a1b2"}) resp = client(rec).save_spool({"name": "SUNLU005", "brand": "Sunlu", "material": "PLA+", "color": "#1E40AF", "diameter": 1.75, "net": 1000.0, "remaining": 812.5, "note": "spoolman:5"}) assert resp == {"ok": True, "id": "a1b2"} r = rec.requests[0] assert r.method == "POST" and r.url.path == "/api/spools/save" assert r.headers["host"] == "10.10.2.202:8081" assert "origin" not in r.headers and "sec-fetch-site" not in r.headers assert r.headers["x-cc2-request"] == "1" assert "transfer-encoding" not in r.headers assert r.headers["content-type"].startswith("text/plain") assert r.content.decode() == ("name=SUNLU005\nbrand=Sunlu\nmaterial=PLA+\ncolor=#1E40AF\ndiameter=1.75\n" "net=1000\nremaining=812.5\nnote=spoolman:5\n") # every header only once (CC2 Control fails closed on duplicates) names = [k.lower() for k, _ in r.headers.raw] assert len(names) == len(set(names)) def test_get_request_has_no_write_marker_or_origin(): rec = Recorder(body={"enabled": True, "slots": [], "spools": []}) assert client(rec).spools()["enabled"] is True r = rec.requests[0] assert r.method == "GET" and r.url.path == "/api/spools" assert r.headers["host"] == "10.10.2.202:8081" and "origin" not in r.headers def test_assign_and_empty_slot(): rec = Recorder() c = client(rec) c.assign(2, "a1b2") c.assign(2, None) assert rec.requests[0].content == b"slot=2\nspool=a1b2\n" assert rec.requests[1].content == b"slot=2\nspool=\n" def test_enable_sends_bare_word(): rec = Recorder(body={"ok": True, "enabled": True}) client(rec).enable(True) client(rec).enable(False) assert rec.requests[0].url.path == "/api/spools/enable" assert rec.requests[0].content == b"on" and rec.requests[1].content == b"off" assert rec.requests[0].headers["x-cc2-request"] == "1" def test_errors_are_reported_without_crash(): rec = Recorder(400, {"ok": False, "error": "Invalid spool"}) with pytest.raises(CC2Error, match="400 Invalid spool"): client(rec).save_spool({"material": "PLA", "color": "#000000"}) def boom(request): raise httpx.ConnectError("refused") with pytest.raises(CC2Error, match="nicht erreichbar"): CC2Control("http://10.10.2.202:8081", transport=httpx.MockTransport(boom)).spools() def test_text_cleaning(): assert clean_text('Sun"lu\\ \x01PLA\n') == "Sun lu PLA" long = "ä" * 60 # 120 bytes out = clean_text(long, 96) assert len(out.encode()) <= 96 and out == "ä" * 48 body = encode_body({"name": "x" * 200, "material": "M" * 50}) name, material = body.splitlines() assert len(name) == len("name=") + 96 and len(material) == len("material=") + 32 assert encode_body({"note": "a=b", "archived": "0", "skip": None}) == "note=a=b\narchived=0\n" def test_spoolman_note_and_trays(): assert spoolman_id_of({"note": "spoolman:17"}) == 17 assert spoolman_id_of({"note": "gekauft 2025 spoolman:3 rest"}) == 3 assert spoolman_id_of({"note": "spoolman:x"}) is None canvas = {"telemetry": {"result": {"canvas_info": {"canvas_list": [{"tray_list": [ {"tray_id": 0, "status": 1}, {"tray_id": 1, "status": 0}, {"tray_id": 2, "status": 2}, {"tray_id": 3}]}]}}}} assert tray_statuses(canvas) == {0: 1, 1: 0, 2: 2} assert tray_statuses({}) == {} def test_spoolman_requests(): seen = [] def handler(request): seen.append(request) return httpx.Response(200, json={"id": 5}) sm = Spoolman("http://10.10.6.10:7912", transport=httpx.MockTransport(handler)) sm.spool(5) sm.update_spool(5, {"location": "CC2 Slot 1"}) sm.use(5, 12.3456) assert [(r.method, r.url.path) for r in seen] == [ ("GET", "/api/v1/spool/5"), ("PATCH", "/api/v1/spool/5"), ("PUT", "/api/v1/spool/5/use")] assert json.loads(seen[1].content) == {"location": "CC2 Slot 1"} assert json.loads(seen[2].content) == {"use_weight": 12.346}