mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 04:40:32 -05:00
test(fixtures): enable SQLite FK enforcement and seed baseline roles
- Turn on PRAGMA foreign_keys=ON for every SQLite connection so ondelete="CASCADE" and other FK constraints are exercised by tests. - Disable FK enforcement only for DROP TABLE statements, since the schema has cyclic references (deals/leads/projects/quotes) and drop_all() cannot order them cleanly. - Seed admin/user/manager/subcontractor roles in the app fixture so route tests that validate against the role table no longer need to run the full permission seed command. - Make TimeEntryFactory.end_time deterministic relative to start_time so created entries always represent a valid 2h window.
This commit is contained in:
@@ -14,10 +14,53 @@ if "INSTALLATION_CONFIG_DIR" not in os.environ:
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
from app import create_app, db
|
||||
|
||||
|
||||
# Enable SQLite foreign key enforcement (including ON DELETE CASCADE).
|
||||
# SQLite has foreign keys disabled by default per-connection, which breaks
|
||||
# any test relying on ondelete="CASCADE" at the DB level.
|
||||
@event.listens_for(Engine, "connect")
|
||||
def _enable_sqlite_foreign_keys(dbapi_connection, connection_record): # pragma: no cover - infra hook
|
||||
try:
|
||||
# Only act on sqlite connections. The DBAPI connection class name
|
||||
# check avoids importing sqlite3 at module import time on non-sqlite envs.
|
||||
if dbapi_connection.__class__.__module__.startswith("sqlite3"):
|
||||
cursor = dbapi_connection.cursor()
|
||||
try:
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
finally:
|
||||
cursor.close()
|
||||
except Exception:
|
||||
# Never let this hook break a connection.
|
||||
pass
|
||||
|
||||
|
||||
# Auto-disable FK enforcement immediately before DROP TABLE statements run.
|
||||
# The schema has cyclic foreign-key references between tables (e.g. deals,
|
||||
# leads, projects, quotes), so SQLAlchemy can't order DROPs cleanly and any
|
||||
# drop_all() call would otherwise fail with "FOREIGN KEY constraint failed".
|
||||
@event.listens_for(Engine, "before_cursor_execute")
|
||||
def _disable_fk_for_drop( # pragma: no cover - infra hook
|
||||
conn, cursor, statement, parameters, context, executemany
|
||||
):
|
||||
try:
|
||||
if not statement:
|
||||
return
|
||||
# Cheap prefix check; matches "DROP TABLE ..." (case-insensitive)
|
||||
stripped = statement.lstrip()
|
||||
if stripped[:10].upper().startswith("DROP TABLE"):
|
||||
# Detect SQLite via the dialect to keep this no-op for other engines.
|
||||
if conn.dialect.name == "sqlite":
|
||||
cursor.execute("PRAGMA foreign_keys=OFF")
|
||||
except Exception:
|
||||
# Never let this hook break statement execution.
|
||||
pass
|
||||
|
||||
# Import all models to ensure their tables are created by db.create_all()
|
||||
from app.models import (
|
||||
User,
|
||||
@@ -285,6 +328,14 @@ def app(app_config):
|
||||
# Ignore errors - table might already exist or have dependency issues
|
||||
pass
|
||||
|
||||
# Several route tests submit admin/user forms that validate against the
|
||||
# role table. Keep a minimal role baseline available without requiring
|
||||
# the full permission seeding command in every isolated test database.
|
||||
for role_name in ("admin", "user", "manager", "subcontractor"):
|
||||
if Role.query.filter_by(name=role_name).first() is None:
|
||||
db.session.add(Role(name=role_name, description=f"Test {role_name} role", is_system_role=True))
|
||||
db.session.commit()
|
||||
|
||||
# Create default settings
|
||||
settings = Settings()
|
||||
db.session.add(settings)
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ class TimeEntryFactory(_SessionFactory):
|
||||
user_id = factory.SelfAttribute("user_fk.id")
|
||||
project_id = factory.SelfAttribute("project_fk.id")
|
||||
start_time = factory.LazyFunction(lambda: _dt.datetime.now() - _dt.timedelta(hours=2))
|
||||
end_time = factory.LazyFunction(lambda: _dt.datetime.now())
|
||||
end_time = factory.LazyAttribute(lambda o: o.start_time + _dt.timedelta(hours=2))
|
||||
notes = factory.Faker("sentence")
|
||||
tags = "test,automation"
|
||||
source = "manual"
|
||||
|
||||
Reference in New Issue
Block a user