| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- """Regression tests for finance tables on upgraded databases."""
- import os
- from unittest.mock import patch
- import pytest
- from sqlalchemy import text
- from sqlalchemy.ext.asyncio import create_async_engine
- from backend.app.core.database import _migrate_create_finance_indexes, _migrate_create_finance_tables
- EXPECTED_TABLES = {
- "cost_centers",
- "wallet_transactions",
- "budget_reservations",
- "cost_center_members",
- "cost_center_invitations",
- "user_wallets",
- }
- @pytest.mark.asyncio
- async def test_finance_tables_are_created_idempotently_on_sqlite():
- engine = create_async_engine("sqlite+aiosqlite:///:memory:")
- try:
- async with engine.begin() as conn:
- with patch("backend.app.core.database.is_sqlite", return_value=True):
- await _migrate_create_finance_tables(conn)
- await _migrate_create_finance_tables(conn)
- rows = await conn.execute(
- text(
- "SELECT name FROM sqlite_master "
- "WHERE type = 'table' AND name IN "
- "('cost_centers', 'wallet_transactions', 'budget_reservations', "
- "'cost_center_members', 'cost_center_invitations', 'user_wallets')"
- )
- )
- assert {row[0] for row in rows} == EXPECTED_TABLES
- finally:
- await engine.dispose()
- @pytest.mark.asyncio
- async def test_legacy_cost_center_indexes_are_delayed_until_columns_exist():
- engine = create_async_engine("sqlite+aiosqlite:///:memory:")
- try:
- async with engine.begin() as conn:
- await conn.execute(text("CREATE TABLE cost_centers (id INTEGER PRIMARY KEY, name VARCHAR(150) NOT NULL)"))
- with patch("backend.app.core.database.is_sqlite", return_value=True):
- await _migrate_create_finance_tables(conn)
- await conn.execute(text("ALTER TABLE cost_centers ADD COLUMN code VARCHAR(32)"))
- await _migrate_create_finance_indexes(conn)
- result = await conn.execute(
- text("SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'ix_cost_centers_code'")
- )
- assert result.scalar_one() == "ix_cost_centers_code"
- finally:
- await engine.dispose()
- @pytest.mark.asyncio
- async def test_postgres_finance_ddl_uses_postgres_types():
- statements: list[str] = []
- async def capture_statement(_conn, sql: str) -> None:
- statements.append(sql)
- with (
- patch("backend.app.core.database.is_sqlite", return_value=False),
- patch("backend.app.core.database._safe_execute", side_effect=capture_statement),
- ):
- await _migrate_create_finance_tables(object())
- create_statements = [sql for sql in statements if "CREATE TABLE" in sql]
- assert len(create_statements) == len(EXPECTED_TABLES)
- assert all("IF NOT EXISTS" in sql for sql in create_statements)
- assert all("DATETIME" not in sql for sql in create_statements)
- assert all("id SERIAL PRIMARY KEY" in sql for sql in create_statements)
- assert "TIMESTAMP" in "\n".join(create_statements)
- created_tables = {
- sql.split("CREATE TABLE IF NOT EXISTS", 1)[1].split("(", 1)[0].strip() for sql in create_statements
- }
- assert created_tables == EXPECTED_TABLES
- @pytest.mark.asyncio
- async def test_finance_tables_are_created_idempotently_on_postgres():
- database_url = os.getenv("BAMBUDDY_TEST_POSTGRES_URL")
- if not database_url:
- pytest.skip("BAMBUDDY_TEST_POSTGRES_URL is not configured")
- engine = create_async_engine(database_url)
- try:
- async with engine.begin() as conn:
- # Minimal pre-billing schema: these are the only tables referenced
- # by foreign keys in the new finance tables.
- await conn.execute(text("CREATE TABLE users (id SERIAL PRIMARY KEY)"))
- await conn.execute(text("CREATE TABLE print_archives (id SERIAL PRIMARY KEY)"))
- await conn.execute(text("CREATE TABLE print_queue (id SERIAL PRIMARY KEY)"))
- with patch("backend.app.core.database.is_sqlite", return_value=False):
- await _migrate_create_finance_tables(conn)
- await _migrate_create_finance_tables(conn)
- await _migrate_create_finance_indexes(conn)
- await _migrate_create_finance_indexes(conn)
- rows = await conn.execute(
- text(
- "SELECT table_name FROM information_schema.tables "
- "WHERE table_schema = 'public' AND table_name = ANY(:tables)"
- ),
- {"tables": sorted(EXPECTED_TABLES)},
- )
- timestamp_type = await conn.execute(
- text(
- "SELECT data_type FROM information_schema.columns "
- "WHERE table_schema = 'public' "
- "AND table_name = 'cost_centers' AND column_name = 'created_at'"
- )
- )
- assert {row[0] for row in rows} == EXPECTED_TABLES
- assert timestamp_type.scalar_one() == "timestamp without time zone"
- finally:
- await engine.dispose()
|