Files
cc2-spoolman-bridge/tests/test_cc2control.py
T
Jonas MenzelandClaude Opus 5.5 a9041eb538
CI / test (push) Successful in 1m6s
CI / image (push) Successful in 43s
Multiple printers, printer/slot in Spoolman, kept in sync
- One worker per printer (name, IP, CC2 Control URL, own pinned host key);
  a spool sits in at most one slot of one printer, moves between printers
- Spoolman: location "<printer> Slot N" plus extra fields "drucker"/"slot"
  (created by the bridge), cleared and "Lager" on removal
- Full comparison every minute: the printer wins over manual edits of loaded
  spools; stale printer locations go to storage; own locations untouched
- Automatic migration of v1 settings, state and host key
- Status hint when a tag is still in a tray but unmapped (e.g. after
  "Take out" in CC2 Control)
- SSH test: connection drops during exec give a clean error, not a 500
- Mock environment with several printers; tests for all of the above

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
2026-10-25 03:53:01 +01:00

116 lines
4.6 KiB
Python

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}