from flask import Flask from app.api import report_bp, simulation_bp from app.api.auth import auth_bp from app.db import Base, create_session_factory from app.models.project import ProjectManager from app.models.task import TaskManager from app.services.identity import IdentityRepository, PasswordService from app.services.report_agent import Report, ReportManager, ReportStatus from app.services.simulation_manager import SimulationManager PASSWORD = "correct horse battery staple" def make_resource_app(): from sqlalchemy import create_engine engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) session_factory = create_session_factory(engine) app = Flask(__name__) app.config.update(TESTING=True, SECRET_KEY="test-secret", SESSION_COOKIE_SECURE=False) app.extensions["crowdsight_session_factory"] = session_factory app.register_blueprint(auth_bp, url_prefix="/api/auth") app.register_blueprint(simulation_bp, url_prefix="/api/simulation") app.register_blueprint(report_bp, url_prefix="/api/report") with session_factory() as session: repo = IdentityRepository(session) organization = repo.create_organization(name="Org A", slug="org-a") user = repo.create_user( email="user-a@example.com", password_hash=PasswordService.hash_password(PASSWORD), ) repo.create_membership(user.id, organization.id, "user") user_b = repo.create_user( email="user-b@example.com", password_hash=PasswordService.hash_password(PASSWORD), ) repo.create_membership(user_b.id, organization.id, "user") session.commit() return app, engine, organization.id, user.id, user_b.id def login(client): response = client.post( "/api/auth/login", json={"email": "user-a@example.com", "password": PASSWORD}, ) assert response.status_code == 200 def csrf_headers(client): return {"X-CSRF-Token": client.get_cookie("crowdsight_csrf").value} def test_simulation_and_report_routes_require_authentication(): app, engine, _organization_id, _user_id, _user_b_id = make_resource_app() try: client = app.test_client() protected_requests = [ ("GET", "/api/simulation/list", None), ("GET", "/api/simulation/sim_other", None), ("POST", "/api/simulation/create", {"project_id": "project_other"}), ("GET", "/api/report/list", None), ("GET", "/api/report/report_other", None), ("POST", "/api/report/generate", {"simulation_id": "sim_other"}), ("POST", "/api/report/generate/status", {"task_id": "task_other"}), ] for method, path, payload in protected_requests: response = client.open(path, method=method, json=payload) assert response.status_code == 401, (method, path, response.status_code) finally: engine.dispose() def test_authenticated_mutations_still_require_csrf(): app, engine, _organization_id, _user_id, _user_b_id = make_resource_app() try: client = app.test_client() login(client) response = client.post( "/api/simulation/create", json={"project_id": "project_other"}, ) assert response.status_code == 403 assert response.get_json()["error_code"] == "csrf_failed" response = client.post( "/api/report/generate", json={"simulation_id": "sim_other"}, headers=csrf_headers(client), ) assert response.status_code in {404, 400} finally: engine.dispose() def test_user_cannot_read_or_create_from_another_users_simulation_and_report(): app, engine, organization_id, user_a_id, user_b_id = make_resource_app() original_projects_dir = ProjectManager.PROJECTS_DIR original_simulations_dir = SimulationManager.SIMULATION_DATA_DIR original_reports_dir = ReportManager.REPORTS_DIR from tempfile import TemporaryDirectory with TemporaryDirectory() as temp_dir: try: ProjectManager.PROJECTS_DIR = f"{temp_dir}/projects" SimulationManager.SIMULATION_DATA_DIR = f"{temp_dir}/simulations" ReportManager.REPORTS_DIR = f"{temp_dir}/reports" project_a = ProjectManager.create_project( "A", organization_id=organization_id, owner_user_id=user_a_id ) project_a.graph_id = "graph-a" ProjectManager.save_project(project_a) project_b = ProjectManager.create_project( "B", organization_id=organization_id, owner_user_id=user_b_id ) project_b.graph_id = "graph-b" ProjectManager.save_project(project_b) manager = SimulationManager() simulation_a = manager.create_simulation(project_a.project_id, "graph-a") simulation_b = manager.create_simulation(project_b.project_id, "graph-b") report_b = Report( report_id="report-b", simulation_id=simulation_b.simulation_id, graph_id="graph-b", simulation_requirement="private requirement", status=ReportStatus.COMPLETED, markdown_content="private report", created_at="2026-08-23T00:00:00", ) ReportManager.save_report(report_b) client = app.test_client() login(client) assert client.get(f"/api/simulation/{simulation_b.simulation_id}").status_code == 404 listed_simulations = client.get("/api/simulation/list").get_json()["data"] assert all(item["simulation_id"] != simulation_b.simulation_id for item in listed_simulations) assert client.get(f"/api/report/{report_b.report_id}").status_code == 404 listed_reports = client.get("/api/report/list").get_json()["data"] assert all(item["report_id"] != report_b.report_id for item in listed_reports) response = client.post( "/api/simulation/create", json={"project_id": project_b.project_id}, headers=csrf_headers(client), ) assert response.status_code == 404 task_id_b = TaskManager().create_task( "report_generate", metadata={ "organization_id": organization_id, "owner_user_id": user_b_id, "simulation_id": simulation_b.simulation_id, }, ) response = client.post( "/api/report/generate/status", json={"task_id": task_id_b}, headers=csrf_headers(client), ) assert response.status_code == 404 finally: ProjectManager.PROJECTS_DIR = original_projects_dir SimulationManager.SIMULATION_DATA_DIR = original_simulations_dir ReportManager.REPORTS_DIR = original_reports_dir engine.dispose()