import pytest from flask import Flask from sqlalchemy import create_engine from app.db import Base, create_session_factory from app.models.task import TaskManager, TaskStatus from app.security.policy import Role from app.services.identity import IdentityRepository def test_task_state_survives_task_manager_reconfiguration(): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) factory = create_session_factory(engine) try: with factory() as session: repo = IdentityRepository(session) org = repo.create_organization(name="Jobs", slug="jobs-org") user = repo.create_user(email="jobs@example.com") repo.create_membership(user.id, org.id, Role.ADMIN) session.commit() TaskManager.configure(factory) first_manager = TaskManager() task_id = first_manager.create_task( "durable_test", metadata={"organization_id": org.id, "owner_user_id": user.id}, ) first_manager.update_task( task_id, status=TaskStatus.PROCESSING, progress=42, message="working", progress_detail={"stage": "one"}, ) TaskManager.configure(factory) restarted_manager = TaskManager() task = restarted_manager.get_task(task_id) assert task is not None assert task.status is TaskStatus.PROCESSING assert task.progress == 42 assert task.message == "working" assert task.progress_detail == {"stage": "one"} finally: TaskManager.configure(None) engine.dispose() def test_task_manager_query_filters_are_tenant_scoped(): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) factory = create_session_factory(engine) try: TaskManager.configure(factory) manager = TaskManager() task_a = manager.create_task("alpha", {"organization_id": "org-a", "owner_user_id": "user-a"}) task_b = manager.create_task("beta", {"organization_id": "org-b", "owner_user_id": "user-b"}) assert manager.get_task(task_a, organization_id="org-a") is not None assert manager.get_task(task_b, organization_id="org-a") is None assert manager.get_task(task_a, organization_id="org-a", owner_user_id="user-b") is None assert [task.metadata["organization_id"] for task in manager.list_tasks(organization_id="org-a")] == ["org-a"] assert [task.metadata["owner_user_id"] for task in manager.list_tasks(organization_id="org-a", owner_user_id="user-a")] == ["user-a"] assert manager.list_tasks(organization_id="org-a", owner_user_id="user-b") == [] finally: TaskManager.configure(None) engine.dispose() def test_task_manager_fails_closed_inside_app_without_session_factory(): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) factory = create_session_factory(engine) app = Flask("missing-session-factory") try: TaskManager.configure(factory) with app.app_context(): with pytest.raises(RuntimeError, match="task_session_factory_required"): TaskManager() finally: TaskManager.configure(None) engine.dispose() def test_task_manager_binds_to_current_app_without_cross_app_leak(): engines = [] apps = [] organization_ids = [] try: for slug in ("app-a", "app-b"): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) factory = create_session_factory(engine) with factory() as session: repo = IdentityRepository(session) organization = repo.create_organization(name=slug, slug=slug) session.commit() app = Flask(slug) app.extensions["crowdsight_session_factory"] = factory engines.append(engine) apps.append(app) organization_ids.append(organization.id) TaskManager.configure(None) with apps[0].app_context(): task_a = TaskManager().create_task( "app_a_task", metadata={"organization_id": organization_ids[0]}, ) with apps[1].app_context(): assert TaskManager().get_task(task_a) is None task_b = TaskManager().create_task( "app_b_task", metadata={"organization_id": organization_ids[1]}, ) with apps[0].app_context(): assert TaskManager().get_task(task_a) is not None assert TaskManager().get_task(task_b) is None finally: TaskManager.configure(None) for engine in engines: engine.dispose() def test_reused_task_manager_fails_closed_across_app_contexts(): engines = [] apps = [] organization_ids = [] try: for slug in ("reuse-a", "reuse-b"): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) factory = create_session_factory(engine) with factory() as session: repo = IdentityRepository(session) organization = repo.create_organization(name=slug, slug=slug) session.commit() app = Flask(slug) app.extensions["crowdsight_session_factory"] = factory engines.append(engine) apps.append(app) organization_ids.append(organization.id) with apps[0].app_context(): manager = TaskManager() task_id = manager.create_task("reuse_task", {"organization_id": organization_ids[0]}) with apps[1].app_context(): with pytest.raises(RuntimeError, match="task_app_context_mismatch"): manager.get_task(task_id) finally: TaskManager.configure(None) for engine in engines: engine.dispose() def test_explicit_task_manager_factory_must_match_current_app(): engine_a = create_engine("sqlite+pysqlite:///:memory:") engine_b = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine_a) Base.metadata.create_all(engine_b) factory_a = create_session_factory(engine_a) factory_b = create_session_factory(engine_b) app_b = Flask("explicit-b") app_b.extensions["crowdsight_session_factory"] = factory_b try: with app_b.app_context(): with pytest.raises(RuntimeError, match="task_session_factory_mismatch"): TaskManager(session_factory=factory_a) finally: TaskManager.configure(None) engine_a.dispose() engine_b.dispose() def test_prebound_task_manager_fails_closed_inside_other_app(): engine_a = create_engine("sqlite+pysqlite:///:memory:") engine_b = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine_a) Base.metadata.create_all(engine_b) factory_a = create_session_factory(engine_a) factory_b = create_session_factory(engine_b) app_b = Flask("prebound-b") app_b.extensions["crowdsight_session_factory"] = factory_b manager = TaskManager(session_factory=factory_a) try: with app_b.app_context(): with pytest.raises(RuntimeError, match="task_session_factory_mismatch"): manager.get_task("not-in-scope") finally: TaskManager.configure(None) engine_a.dispose() engine_b.dispose() def test_app_bound_task_manager_supports_background_use_without_context(): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) factory = create_session_factory(engine) app = Flask("background-use") app.extensions["crowdsight_session_factory"] = factory try: with app.app_context(): manager = TaskManager() task_id = manager.create_task("background_task", {"organization_id": "org-bg"}) assert manager.get_task(task_id) is not None finally: TaskManager.configure(None) engine.dispose()