test_wallet_transactions_user_migration.py 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """wallet_transactions.user_id becomes nullable, ON DELETE SET NULL, on existing SQLite databases.
  2. It was NOT NULL with ON DELETE CASCADE: deleting a user deleted their ledger
  3. entries, which freed what they had spent of a shared cost centre's budget.
  4. The entries stay now, without a user. On SQLite the NOT NULL has to go from
  5. tables made before, or the delete fails on it. (PostgreSQL is checked by
  6. scripts/check_postgres_upgrade.py.)
  7. """
  8. import pytest
  9. from sqlalchemy import text
  10. from sqlalchemy.ext.asyncio import create_async_engine
  11. from backend.app.core import database
  12. # As create_all made the table from the old model ...
  13. CREATED_BY_THE_MODEL = """
  14. CREATE TABLE wallet_transactions (
  15. \tid INTEGER NOT NULL,
  16. \tuser_id INTEGER NOT NULL,
  17. \tcost_center_id INTEGER,
  18. \ttransaction_type VARCHAR(40) NOT NULL,
  19. \tamount NUMERIC(14, 2) NOT NULL,
  20. \tcreated_by_user_id INTEGER,
  21. \tPRIMARY KEY (id),
  22. \tFOREIGN KEY(user_id) REFERENCES users (id) ON DELETE CASCADE,
  23. \tFOREIGN KEY(cost_center_id) REFERENCES cost_centers (id) ON DELETE SET NULL,
  24. \tFOREIGN KEY(created_by_user_id) REFERENCES users (id) ON DELETE SET NULL
  25. )
  26. """
  27. # ... and as the finance migration's own CREATE TABLE made it.
  28. CREATED_BY_THE_MIGRATION = """
  29. CREATE TABLE wallet_transactions (
  30. id INTEGER PRIMARY KEY,
  31. user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  32. cost_center_id INTEGER REFERENCES cost_centers(id) ON DELETE SET NULL,
  33. transaction_type VARCHAR(40) NOT NULL,
  34. amount NUMERIC(14,2) NOT NULL,
  35. created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL
  36. )
  37. """
  38. @pytest.fixture(autouse=True)
  39. def sqlite_dialect(monkeypatch):
  40. monkeypatch.setattr(database, "is_sqlite", lambda: True)
  41. @pytest.mark.asyncio
  42. @pytest.mark.parametrize("legacy", [CREATED_BY_THE_MODEL, CREATED_BY_THE_MIGRATION], ids=["model", "migration"])
  43. async def test_the_user_column_takes_null_and_says_set_null(legacy):
  44. engine = create_async_engine("sqlite+aiosqlite:///:memory:")
  45. try:
  46. async with engine.begin() as conn:
  47. await conn.execute(text(legacy))
  48. await conn.execute(
  49. text(
  50. "INSERT INTO wallet_transactions (id, user_id, transaction_type, amount) VALUES (1, 7, 'deposit', 5)"
  51. )
  52. )
  53. await database._migrate_wallet_transactions_keep_deleted_users(conn)
  54. await database._migrate_wallet_transactions_keep_deleted_users(conn) # a restart changes nothing
  55. await conn.execute(text("UPDATE wallet_transactions SET user_id = NULL WHERE id = 1"))
  56. assert (await conn.execute(text("SELECT user_id, amount FROM wallet_transactions"))).one() == (None, 5)
  57. sql = (
  58. await conn.execute(text("SELECT sql FROM sqlite_master WHERE name = 'wallet_transactions'"))
  59. ).scalar_one()
  60. assert "CASCADE" not in sql
  61. # Only the user column changed.
  62. assert "transaction_type VARCHAR(40) NOT NULL" in sql
  63. assert sql.count("ON DELETE SET NULL") == 3
  64. finally:
  65. await engine.dispose()