85 lines
5.4 KiB
Python
85 lines
5.4 KiB
Python
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_local_development_admin_accepts_operator_supplied_short_password():
|
|
session_factory = setup_db()
|
|
with session_factory() as session:
|
|
admin = create_admin(session, "admin", "admin")
|
|
assert admin.username == "admin"
|
|
assert verify_password("admin", admin.password_hash)
|
|
|
|
|
|
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
|