from types import SimpleNamespace import pytest from flask import Flask, g from sqlalchemy import create_engine from app.api import simulation_bp from app.api.auth import auth_bp from app.api.simulation import _simulation_manager_for_request from app.config import Config from app.db import Base, create_session_factory from app.models.memory import MemoryGraph, MemoryNode from app.models.project import ProjectManager from app.security.policy import Role from app.services.identity import IdentityRepository, PasswordService from app.services.memory_entity_reader import LocalEntityReader from app.utils.api_errors import ApiError def make_app(): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) app = Flask(__name__) app.config.update(TESTING=True, SECRET_KEY="test-secret", SESSION_COOKIE_SECURE=False) app.extensions["crowdsight_session_factory"] = create_session_factory(engine) return app, engine def test_local_memory_backend_selects_scoped_reader_factory(monkeypatch): app, engine = make_app() old_backend = Config.MEMORY_BACKEND try: monkeypatch.setattr(Config, "MEMORY_BACKEND", "local") with app.test_request_context("/api/simulation/prepare"): g.auth_context = SimpleNamespace( user=SimpleNamespace(id="user-a"), membership=SimpleNamespace(role=Role.USER), organization=SimpleNamespace(id="org-a"), ) manager = _simulation_manager_for_request() reader = manager.create_entity_reader("graph-a") assert isinstance(reader, LocalEntityReader) assert reader.repository.organization_id == "org-a" reader.close() finally: Config.MEMORY_BACKEND = old_backend engine.dispose() def test_unknown_memory_backend_fails_closed(monkeypatch): app, engine = make_app() old_backend = Config.MEMORY_BACKEND try: monkeypatch.setattr(Config, "MEMORY_BACKEND", "unknown") with app.test_request_context("/api/simulation/prepare"): g.auth_context = SimpleNamespace( user=SimpleNamespace(id="user-a"), membership=SimpleNamespace(role=Role.USER), organization=SimpleNamespace(id="org-a"), ) with pytest.raises(ApiError) as error: _simulation_manager_for_request() assert error.value.code == "invalid_memory_backend" finally: Config.MEMORY_BACKEND = old_backend engine.dispose() def test_local_entity_route_reads_scoped_memory_graph(monkeypatch): app, engine = make_app() app.register_blueprint(auth_bp, url_prefix="/api/auth") app.register_blueprint(simulation_bp, url_prefix="/api/simulation") original_projects_dir = ProjectManager.PROJECTS_DIR old_backend = Config.MEMORY_BACKEND old_zep_key = Config.ZEP_API_KEY from tempfile import TemporaryDirectory with TemporaryDirectory() as temp_dir: try: ProjectManager.PROJECTS_DIR = f"{temp_dir}/projects" with app.extensions["crowdsight_session_factory"]() as session: repo = IdentityRepository(session) org = repo.create_organization(name="Org A", slug="org-a") user = repo.create_user( email="user-a@example.com", password_hash=PasswordService.hash_password("correct horse battery staple"), ) repo.create_membership(user.id, org.id, "user") session.add( MemoryGraph( id="graph-local", organization_id=org.id, project_id="project-local", ) ) session.add( MemoryNode( id="node-local", graph_id="graph-local", canonical_name="Local Alice", normalized_name="local alice", labels=["Person"], summary="Stored locally.", ) ) session.commit() project = ProjectManager.create_project( "Local project", organization_id=org.id, owner_user_id=user.id, ) project.graph_id = "graph-local" ProjectManager.save_project(project) monkeypatch.setattr(Config, "MEMORY_BACKEND", "local") monkeypatch.setattr(Config, "ZEP_API_KEY", None) client = app.test_client() login = client.post( "/api/auth/login", json={"email": "user-a@example.com", "password": "correct horse battery staple"}, ) assert login.status_code == 200 response = client.get("/api/simulation/entities/graph-local") assert response.status_code == 200 entities = response.get_json()["data"]["entities"] assert entities[0]["name"] == "Local Alice" finally: ProjectManager.PROJECTS_DIR = original_projects_dir Config.MEMORY_BACKEND = old_backend Config.ZEP_API_KEY = old_zep_key engine.dispose()