mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
Non-proxy VPs (Archive / Review / Queue) with a target printer set up a live-mirror bridge that forwards the slicer's MQTT and RTSPS auth bytes through to the real printer. The slicer holds one code in its profile (the one it bound the VP with), and that code has to satisfy both the VP listener and the real printer at the far end of the bridge. If the codes diverge the bridge silently fails at the second hop — slicer reaches .49:8883, FINs before sending a ClientHello, retries identically. The wiki framed the code-match requirement as a camera-only concern; it isn't, all bridged protocols inherit. Fix removes the foot-gun instead of re-documenting it. When a target is selected on a non-proxy VP the access-code field switches to a read-only display showing the target's code with an Eye-toggle reveal; the backend auto-inherits on every create / update (any explicit access_code submitted alongside a target is silently overridden as belt-and-braces for non-UI clients). The required-when- enabling check now treats target-set as satisfying the access-code requirement. Standalone (no-target) non-proxy VPs still get the editable input + Save button. One-shot startup migration corrects any pre-existing mismatched rows: SELECTs diverged VPs and logs one INFO line per row for the audit trail, then UPDATEs via correlated subquery. Idempotent and portable between SQLite and Postgres.
238 lines
8.3 KiB
Python
238 lines
8.3 KiB
Python
"""Regression test for the VP access-code sync migration.
|
|
|
|
Non-proxy VPs with a target printer must use the target's access code
|
|
because the live-mirror bridge forwards the slicer's MQTT/RTSPS auth
|
|
bytes through to the real printer. Earlier UIs let the codes diverge,
|
|
producing a VP whose listener accepted the bind but whose bridge then
|
|
failed at the second hop. The migration in ``run_migrations`` rewrites
|
|
mismatched rows on the next boot after upgrade.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
|
|
from backend.app.core.database import run_migrations
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def force_sqlite_dialect(monkeypatch):
|
|
"""Force the SQLite branch regardless of test env settings."""
|
|
from backend.app.core import db_dialect
|
|
|
|
monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
|
|
monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
|
|
from backend.app.core import database as database_module
|
|
|
|
monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
|
|
|
|
|
|
def _register_all_models():
|
|
"""run_migrations touches multiple tables; the full schema must exist."""
|
|
from backend.app.models import ( # noqa: F401
|
|
ams_history,
|
|
ams_label,
|
|
api_key,
|
|
archive,
|
|
color_catalog,
|
|
external_link,
|
|
filament,
|
|
group,
|
|
kprofile_note,
|
|
maintenance,
|
|
notification,
|
|
notification_template,
|
|
print_log,
|
|
print_queue,
|
|
printer,
|
|
project,
|
|
project_bom,
|
|
settings,
|
|
slot_preset,
|
|
smart_plug,
|
|
smart_plug_energy_snapshot,
|
|
spool,
|
|
spool_assignment,
|
|
spool_catalog,
|
|
spool_k_profile,
|
|
spool_usage_history,
|
|
spoolbuddy_device,
|
|
user,
|
|
user_email_pref,
|
|
virtual_printer,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def engine():
|
|
from backend.app.core.database import Base
|
|
|
|
_register_all_models()
|
|
|
|
eng = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
|
|
async with eng.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
yield eng
|
|
await eng.dispose()
|
|
|
|
|
|
async def _seed_printer(engine, printer_id: int, name: str, access_code: str) -> None:
|
|
"""Insert a printer row through the ORM so Python-side defaults
|
|
(nozzle_count, is_active, auto_archive, print_hours_offset, …) all apply
|
|
without us having to mirror every NOT NULL column in raw SQL."""
|
|
from backend.app.models.printer import Printer
|
|
|
|
async with AsyncSession(engine) as session:
|
|
session.add(
|
|
Printer(
|
|
id=printer_id,
|
|
name=name,
|
|
ip_address=f"192.168.1.{printer_id + 100}",
|
|
access_code=access_code,
|
|
serial_number=f"01P00A39180000{printer_id}",
|
|
model="C12",
|
|
)
|
|
)
|
|
await session.commit()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_proxy_vp_with_target_inherits_access_code(engine):
|
|
"""A non-proxy VP with a mismatched access_code gets corrected to match
|
|
the target printer's code on the next boot."""
|
|
await _seed_printer(engine, 1, "Real X1C", "REALCODE")
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers "
|
|
"(id, name, enabled, mode, access_code, target_printer_id, serial_suffix, position) "
|
|
"VALUES (1, 'Queue VP', 0, 'queue', 'OLDVPCDE', 1, '391800001', 1)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
code = (await conn.execute(text("SELECT access_code FROM virtual_printers WHERE id = 1"))).scalar()
|
|
assert code == "REALCODE"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_proxy_vp_access_code_is_left_alone(engine):
|
|
"""Proxy-mode VPs are NOT touched — the proxy already uses the target's
|
|
code transparently at the protocol level, and the model column can
|
|
legitimately hold an unused access_code value."""
|
|
await _seed_printer(engine, 1, "Real X1C", "REALCODE")
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers "
|
|
"(id, name, enabled, mode, access_code, target_printer_id, serial_suffix, position) "
|
|
"VALUES (1, 'Proxy VP', 0, 'proxy', 'PROXYCDE', 1, '391800001', 1)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
code = (await conn.execute(text("SELECT access_code FROM virtual_printers WHERE id = 1"))).scalar()
|
|
assert code == "PROXYCDE"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_already_matching_vp_is_left_alone(engine):
|
|
"""A VP whose code already equals the target's needs no change.
|
|
Confirms the WHERE clause excludes synced rows so re-running is a no-op."""
|
|
await _seed_printer(engine, 1, "Real X1C", "MATCHED1")
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers "
|
|
"(id, name, enabled, mode, access_code, target_printer_id, serial_suffix, position) "
|
|
"VALUES (1, 'Synced VP', 0, 'archive', 'MATCHED1', 1, '391800001', 1)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
# Re-run to prove idempotency.
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
code = (await conn.execute(text("SELECT access_code FROM virtual_printers WHERE id = 1"))).scalar()
|
|
assert code == "MATCHED1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_proxy_vp_without_target_is_left_alone(engine):
|
|
"""No target = no bridge = nothing to derive from. The VP keeps its own code."""
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers "
|
|
"(id, name, enabled, mode, access_code, target_printer_id, serial_suffix, position) "
|
|
"VALUES (1, 'Standalone VP', 0, 'archive', 'STANDALN', NULL, '391800001', 1)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
code = (await conn.execute(text("SELECT access_code FROM virtual_printers WHERE id = 1"))).scalar()
|
|
assert code == "STANDALN"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_null_vp_access_code_with_target_gets_populated(engine):
|
|
"""A VP with no access_code at all (NULL) but a target set is treated
|
|
as a divergence — the migration populates it from the target."""
|
|
await _seed_printer(engine, 1, "Real X1C", "FRESHCDE")
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers "
|
|
"(id, name, enabled, mode, access_code, target_printer_id, serial_suffix, position) "
|
|
"VALUES (1, 'Fresh VP', 0, 'queue', NULL, 1, '391800001', 1)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
code = (await conn.execute(text("SELECT access_code FROM virtual_printers WHERE id = 1"))).scalar()
|
|
assert code == "FRESHCDE"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multi_vp_sync_one_run(engine):
|
|
"""Multiple mismatched VPs against different targets are all corrected
|
|
in a single migration pass."""
|
|
await _seed_printer(engine, 1, "Printer A", "AAAAAAAA")
|
|
await _seed_printer(engine, 2, "Printer B", "BBBBBBBB")
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers "
|
|
"(id, name, enabled, mode, access_code, target_printer_id, serial_suffix, position) "
|
|
"VALUES "
|
|
"(1, 'VP-A', 0, 'archive', 'WRONGAAA', 1, '391800001', 1),"
|
|
"(2, 'VP-B', 0, 'queue', 'WRONGBBB', 2, '391800002', 2)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
result = await conn.execute(text("SELECT id, access_code FROM virtual_printers ORDER BY id"))
|
|
rows = dict(result.fetchall())
|
|
|
|
assert rows[1] == "AAAAAAAA"
|
|
assert rows[2] == "BBBBBBBB"
|