mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
feat(virtual-printer): setup diagnostic + one-click slicer-certificate export
Two recurring virtual-printer support pains, both on the Virtual Printers
settings page.
Setup check: a stethoscope action on each VP card runs a pass/fail/warn/skip
checklist — VP enabled, services running, bind interface still exists, access
code set, target printer (proxy mode), and a live TCP probe of the FTP / MQTT
/ discovery ports on the bind IP. start_server swallows per-service bind
errors, so a service object can exist while nothing is listening; probing the
bind IP from outside is the only reliable signal and it catches the common
"VP not visible in the slicer" bind-IP-conflict and stale-interface cases.
Slicer certificate: virtual printers present a TLS cert signed by a shared CA
the slicer must trust. Until now users had to docker exec in and cat
bbl_ca.crt. A "Slicer certificate" row on the settings card now offers Copy
and Download (bambuddy-virtual-printer-ca.crt) plus the SHA-256 fingerprint.
GET /virtual-printers/ca-certificate returns only the public certificate; the
CA private key never leaves the backend. The CA is generated on demand so the
button works before the first VP is enabled.
Backend:
- services/virtual_printer/diagnostic.py — run_vp_diagnostic + port probes
- schemas/virtual_printer.py — VPDiagnosticResult
- CertificateService.get_ca_certificate_info() + manager helper
- routes: GET /virtual-printers/ca-certificate, /{vp_id}/diagnostic
Frontend:
- VirtualPrinterDiagnosticModal.tsx; stethoscope button on VirtualPrinterCard
- caCert row on VirtualPrinterList; utils/clipboard.ts (shared copy w/
non-secure-context fallback + downloadTextFile), de-duplicating the
existing FQDN-copy logic
- vpDiagnostic.* + virtualPrinter.caCert.* across all 9 locales
9 backend unit tests + 4 route integration tests + 6 frontend tests.
Backend ruff clean, frontend build clean, i18n parity green.
This commit is contained in:
@@ -384,3 +384,58 @@ class TestVirtualPrinterTailscaleToggleAPI:
|
||||
assert enable_resp.json()["tailscale_disabled"] is False
|
||||
assert disable_resp.status_code == 200
|
||||
assert disable_resp.json()["tailscale_disabled"] is True
|
||||
|
||||
|
||||
class TestVirtualPrinterCaCertificateAPI:
|
||||
"""Integration tests for GET /api/v1/virtual-printers/ca-certificate."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_ca_certificate_returns_pem(self, async_client: AsyncClient):
|
||||
"""The shared CA certificate is returned as PEM with identifying metadata."""
|
||||
response = await async_client.get("/api/v1/virtual-printers/ca-certificate")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["pem"].startswith("-----BEGIN CERTIFICATE-----")
|
||||
assert "PRIVATE KEY" not in result["pem"] # never expose the CA key
|
||||
assert len(result["fingerprint_sha256"].split(":")) == 32
|
||||
assert result["not_valid_after"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_ca_certificate_route_precedes_vp_id_route(self, async_client: AsyncClient):
|
||||
"""'ca-certificate' must not be swallowed by the /{vp_id} int route."""
|
||||
response = await async_client.get("/api/v1/virtual-printers/ca-certificate")
|
||||
# A 200 (not 422 from int-parsing "ca-certificate") proves route ordering.
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestVirtualPrinterDiagnosticAPI:
|
||||
"""Integration tests for GET /api/v1/virtual-printers/{vp_id}/diagnostic."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_diagnose_unknown_vp_returns_404(self, async_client: AsyncClient):
|
||||
response = await async_client.get("/api/v1/virtual-printers/999999/diagnostic")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_diagnose_disabled_vp_reports_problems(self, async_client: AsyncClient):
|
||||
"""A freshly created (disabled) VP fails the 'enabled' check."""
|
||||
create_resp = await async_client.post(
|
||||
"/api/v1/virtual-printers",
|
||||
json={"name": "TestDiagVP", "mode": "immediate", "access_code": "12345678"},
|
||||
)
|
||||
assert create_resp.status_code == 200
|
||||
vp_id = create_resp.json()["id"]
|
||||
|
||||
response = await async_client.get(f"/api/v1/virtual-printers/{vp_id}/diagnostic")
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["vp_id"] == vp_id
|
||||
assert result["overall"] == "problems"
|
||||
by_id = {c["id"]: c["status"] for c in result["checks"]}
|
||||
assert by_id["enabled"] == "fail"
|
||||
assert by_id["running"] == "skip"
|
||||
|
||||
Reference in New Issue
Block a user