mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
both dialects rather than one. Postgres upgrades never got as far as the finance schema. database.py added on_billing_charge_failed with BOOLEAN DEFAULT 1. The 1 is a SQLite-ism; Postgres answers DatatypeMismatchError, and _safe_execute deliberately re-raises anything that is not an idempotency error, so run_migrations died there and rolled the whole transaction back. No finance tables, no columns, and the app does not start. Six lines above, the same change gets is_voided right with an is_sqlite() branch, so this was an oversight rather than a decision. Now branched the same way. This also explains the four test_security.py::TestBackupKeyFiles failures reporting "column print_archives.cost_center_id does not exist". That column's migration exists and works -- it simply never ran, because every startup aborted before committing. Reproduced against Postgres 16 by building a pre-billing schema from dev and upgrading over it: fails without this, completes with it, and re-running the migrations or starting from an empty database are both clean. test_billing_run_id_migration.py failed on any Postgres-configured checkout. It builds its own SQLite engine, but run_migrations branches on the global dialect rather than the connection in hand, so on a box whose DATABASE_URL points at Postgres it emitted md5(random()::text) and btrim() into SQLite. Given the same fixture test_ldap_migration.py already carries for exactly this reason. The suite now agrees across dialects -- 9190 passed either way, where it used to be 9184 on one and 9183 on the other. The kill switch could not tell a print Bambuddy started from one it merely watched. Authorization fell back to a print_archives row in status="printing" matched on subtask_id. But on_print_start archives every print it observes, including ones started from Bambu Studio or Handy, and stamps them with the same status and subtask_id -- the code says as much where it notes "a print Bambuddy didn't dispatch". So a foreign print became authorized the moment its 3MF finished downloading, and _active_prints was rehydrated from it, making that permanent. The switch fired only inside the download race, and never afterwards. Neither test caught it: one stubs the authorization call to False, the other stubs the query to return an archive, so the real lookup was never exercised against a foreign print. Authorization now requires a marker Bambuddy writes itself: billing_run_id, minted per dispatch in the scheduler, or created_by_id carried over from the queue item. Failing that, it looks for a queue row in status="printing" on that printer -- committed before the MQTT send, and the only durable trace a library-file dispatch leaves, since those have no archive at send time and the row created for them moments later carries neither marker. That row cannot be tied to a subtask_id, so it defers rather than authorizes. Deferring also closes a false positive the previous version shared: a restart in the window between the send and the download left no archive at all, and a Bambuddy print was stopped as unauthorized. Stopping a print is irreversible and declining to act costs a log line, so ambiguity resolves that way. Tests cover an unmarked archive not being authorization and not entering _active_prints, either marker alone authorizing and rehydrating the fast path without touching the queue, an unmarked archive with a live dispatch deferring, a dispatch not yet archived deferring, and nothing at all being unauthorized.
53 lines
2.5 KiB
Python
53 lines
2.5 KiB
Python
"""Migration coverage for durable per-dispatch billing identities."""
|
|
|
|
import pytest
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
|
import backend.app.models # noqa: F401 - populate Base.metadata
|
|
import backend.app.models.external_link # noqa: F401 - required by a legacy ALTER in run_migrations
|
|
import backend.app.models.print_log # noqa: F401 - required by a legacy ALTER in run_migrations
|
|
from backend.app.core.database import Base, run_migrations
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def force_sqlite_dialect(monkeypatch):
|
|
"""The engine below is SQLite, but settings.database_url may point at Postgres in a
|
|
dev config — and run_migrations branches on the global dialect, not on the
|
|
connection. Without this the Postgres branch runs against SQLite and the migration
|
|
fails on Postgres-only syntax. Same fixture as test_ldap_migration.py."""
|
|
from backend.app.core import db_dialect
|
|
|
|
monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
|
|
monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
|
|
# database.py imported is_sqlite at module load time — patch there too.
|
|
from backend.app.core import database as database_module
|
|
|
|
monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_billing_run_columns_and_legacy_archive_index_are_migrated(tmp_path):
|
|
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'billing-run.db'}")
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
await run_migrations(conn)
|
|
|
|
queue_columns = {row[1] for row in (await conn.execute(text("PRAGMA table_info(print_queue)"))).all()}
|
|
archive_columns = {row[1] for row in (await conn.execute(text("PRAGMA table_info(print_archives)"))).all()}
|
|
notification_columns = {
|
|
row[1] for row in (await conn.execute(text("PRAGMA table_info(notification_providers)"))).all()
|
|
}
|
|
archive_index_sql = await conn.scalar(
|
|
text("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'uq_wallet_transactions_archive'")
|
|
)
|
|
|
|
assert "billing_run_id" in queue_columns
|
|
assert "billing_run_id" in archive_columns
|
|
assert "on_billing_charge_failed" in notification_columns
|
|
assert archive_index_sql is not None
|
|
assert "WHERE print_run_id IS NULL" in archive_index_sql
|
|
finally:
|
|
await engine.dispose()
|