| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- """Regression test for #794 — LDAP auto-provisioning on legacy SQLite schemas.
- Pre-LDAP databases created the `users` table with `password_hash VARCHAR(255) NOT NULL`.
- The LDAP provisioning path inserts users with `password_hash=None`, which crashes on
- upgrade until the migration strips the NOT NULL constraint.
- """
- from __future__ import annotations
- import pytest
- from sqlalchemy import text
- from sqlalchemy.exc import IntegrityError
- from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
- from backend.app.core.database import run_migrations
- @pytest.fixture(autouse=True)
- def force_sqlite_dialect(monkeypatch):
- """The test engine is SQLite but settings.database_url may point to Postgres in dev
- configs — that would make run_migrations take the Postgres branch and skip the
- SQLite-specific writable_schema patch we're verifying. Force the sqlite dialect."""
- 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.fixture
- async def legacy_engine():
- """Simulate an older install by creating all current tables via create_all, then
- dropping the `users` table and re-creating it with the legacy NOT NULL schema.
- This matches the real upgrade path — everything else in the DB looks modern, only
- the users table carries a stale constraint."""
- # Import every model so Base.metadata knows about them (same set as conftest).
- from backend.app.core.database import Base
- 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_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,
- )
- engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.create_all)
- # Drop the users table created from the current (nullable) model and replace it
- # with the pre-LDAP schema that real upgrading installations have on disk.
- await conn.execute(text("DROP TABLE IF EXISTS user_groups"))
- await conn.execute(text("DROP TABLE users"))
- await conn.execute(
- text("""
- CREATE TABLE users (
- id INTEGER PRIMARY KEY,
- username VARCHAR(100) NOT NULL UNIQUE,
- password_hash VARCHAR(255) NOT NULL,
- role VARCHAR(20) NOT NULL DEFAULT 'user',
- is_active BOOLEAN NOT NULL DEFAULT 1,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
- )
- """)
- )
- yield engine
- await engine.dispose()
- async def test_legacy_schema_rejects_null_password_before_migration(legacy_engine):
- """Sanity check: without the migration, inserting a NULL password_hash fails.
- Guards against a false-positive where a future schema change silently allows NULL
- and the real migration test below becomes meaningless.
- """
- async with legacy_engine.begin() as conn:
- with pytest.raises(IntegrityError):
- await conn.execute(
- text(
- "INSERT INTO users (username, password_hash, role, is_active) "
- "VALUES ('ldap_alice', NULL, 'user', 1)"
- )
- )
- async def test_migration_allows_null_password_hash_for_ldap_users(legacy_engine):
- """After running migrations on a legacy DB, LDAP users (password_hash=NULL) insert
- successfully — reproduces and verifies the #794 bug reported by DylanBrass."""
- async with legacy_engine.begin() as conn:
- await run_migrations(conn)
- session_maker = async_sessionmaker(legacy_engine, class_=AsyncSession, expire_on_commit=False)
- async with session_maker() as session:
- await session.execute(
- text(
- "INSERT INTO users (username, email, password_hash, role, auth_source, is_active) "
- "VALUES (:u, :e, NULL, 'user', 'ldap', 1)"
- ),
- {"u": "ldap_bob", "e": "bob@example.com"},
- )
- await session.commit()
- result = await session.execute(
- text("SELECT username, password_hash, auth_source FROM users WHERE username = 'ldap_bob'")
- )
- row = result.one()
- assert row.username == "ldap_bob"
- assert row.password_hash is None
- assert row.auth_source == "ldap"
- async def test_migration_is_idempotent(legacy_engine):
- """Running migrations twice must not break the writable_schema patch."""
- async with legacy_engine.begin() as conn:
- await run_migrations(conn)
- async with legacy_engine.begin() as conn:
- await run_migrations(conn)
- session_maker = async_sessionmaker(legacy_engine, class_=AsyncSession, expire_on_commit=False)
- async with session_maker() as session:
- await session.execute(
- text(
- "INSERT INTO users (username, password_hash, role, auth_source, is_active) "
- "VALUES ('ldap_carol', NULL, 'user', 'ldap', 1)"
- )
- )
- await session.commit()
|