from datetime import datetime, timedelta, timezone from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.pool import StaticPool from sqlalchemy.orm import sessionmaker from central.app.human_auth import create_admin, create_session, hash_password, verify_password from central.app.main import create_app from central.app.models import Agent, AgentEvent, AgentSnapshot, Base, Installation, Tenant from central.app.events import purge_expired_events, record_event, sanitize_event from central.app.installation_identity import compute_agent_health from types import SimpleNamespace def setup_db(): engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool) Base.metadata.create_all(engine) return sessionmaker(bind=engine, expire_on_commit=False) def seed(session_factory): with session_factory() as session: tenant = Tenant(name="Acme"); session.add(tenant); session.flush() agent = Agent(tenant_id=tenant.id, name="agent", token_hash="x" * 64); session.add(agent); session.flush() installation = Installation(agent_id=agent.id, tenant_id=tenant.id, installation_id="11111111-1111-4111-8111-111111111111", display_name="Acme ERP", hostname="srv-01", last_seen_at=datetime.now(timezone.utc)); session.add(installation); session.flush() session.add(AgentSnapshot(installation_id=installation.id, total_products=10, fiscally_complete=8, safe_blocked=2, blockers={"CFOP": 2})) admin = create_admin(session, "operator", "a-secure-password") return agent.id, admin.id def test_password_and_event_redaction_and_retention(): assert verify_password("a-secure-password", hash_password("a-secure-password")) message, context = sanitize_event("password=bad token=secret", {"password": "bad", "nested": {"api_key": "key"}}) assert "bad" not in message and context["password"] == "[REDACTED]" and context["nested"]["api_key"] == "[REDACTED]" session_factory = setup_db() with session_factory() as session: agent_id, _ = seed(session_factory) installation = session.query(Installation).first() old = record_event(session, tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="INFO", event_type="HEARTBEAT", message="old", occurred_at=datetime.now(timezone.utc) - timedelta(days=100)) keep = record_event(session, tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="CRITICAL", event_type="CONNECTION_ERROR", message="keep", occurred_at=datetime.now(timezone.utc) - timedelta(days=100), is_audit=True) session.commit() assert purge_expired_events(session, retention_days=90) == 1 assert session.get(AgentEvent, old.id) is None and session.get(AgentEvent, keep.id) is not None def test_health_model_separates_safe_blocked_from_technical_failure(): now = datetime.now(timezone.utc) recent = SimpleNamespace(last_seen_at=now, failed_products=0, last_job_status="COMPLETED") blocked = SimpleNamespace(last_seen_at=now, failed_products=0, last_job_status="COMPLETED") failed = SimpleNamespace(last_seen_at=now, failed_products=1, last_job_status="FAILED", agent_health="FAILED") stale = SimpleNamespace(last_seen_at=now - timedelta(hours=2), failed_products=0, last_job_status="COMPLETED") assert compute_agent_health(recent)[0] == "ONLINE" assert compute_agent_health(blocked, supplemental=blocked)[0] == "ONLINE" assert compute_agent_health(failed)[0] == "FAILED" assert compute_agent_health(stale)[0] == "OFFLINE" assert compute_agent_health(SimpleNamespace(last_seen_at=None))[0] == "UNKNOWN" def test_human_login_and_dashboard_are_separate_from_agent_bearer(): session_factory = setup_db(); seed(session_factory) client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None)) assert client.get("/", follow_redirects=False).status_code == 303 response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False) assert response.status_code == 303 assert client.get("/v1/dashboard/summary").status_code == 200 assert client.get("/v1/jobs/global").status_code == 200 assert TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None)).get("/v1/dashboard/summary", headers={"Authorization": "Bearer x"}).status_code == 401 assert client.get("/v1/agents").json()["items"][0]["installation_id"] csrf = client.cookies.get("gti_csrf") action = client.post("/v1/agents/1/actions/reprocess", json={"confirm": True, "csrf_token": csrf, "kind": "REPROCESS_ALL"}) assert action.status_code == 200 and action.json()["status"] == "PENDING" denied = client.post("/v1/agents/1/actions/reprocess", json={"confirm": True, "csrf_token": "wrong", "kind": "REPROCESS_ALL"}) assert denied.status_code == 403 assert client.post("/logout").status_code == 403 assert client.post("/logout", data={"csrf_token": csrf}, follow_redirects=False).status_code == 303