Files
GTI_GERENCIADOR_FISCAL/central/tests/test_control_panel_pages.py
T
gtiandClaude Opus 5.5 64d3fc56e2 feat: SSO from GTI Gerenciador and the Gerenciador visual identity
Collaborators logged into GTI Gerenciador open GTI Fiscal without a
second password. The panel signs a 60 s JWT (Ed25519; the Hub holds only
the public key); the Hub checks signature, iss/aud/exp/nbf, maximum
lifetime, nonce against a one-time state cookie set when the flow starts
at GET /login (login-CSRF protection) and a single-use jti
(sso_used_assertions). Collaborators are provisioned in admin_users with
auth_source=GTI_GERENCIADOR and no password; a CHECK constraint makes a
passwordless local account or a federated account with a password
impossible (migration 0021). Sessions last FEDERATED_SESSION_SECONDS
(1 h) and are renewed silently through the panel.

The local admin stays as break-glass at /login/local, audited. Logout ends
only the Hub session. Every SSO success/rejection (stable reason code),
logout and local login is audited with the real client IP, never the
token. verify_password now fails closed on a missing hash instead of
raising.

Visual identity: gti-theme.css is a verbatim copy of the panel's
design system (tokens, sidebar, light/dark theme) plus its tema.js,
sidebar.js and logo; control-panel.css keeps only Hub components, all
colours from the panel tokens. Page structure and features unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 12:21:26 -03:00

425 lines
22 KiB
Python

import re
from datetime import datetime, timedelta, timezone
from fastapi.testclient import TestClient
from sqlalchemy import select
from central.app.blocked_products import upsert_blocker
from central.app.events import record_event
from central.app.main import create_app
from central.app.models import Agent, AgentEvent, AgentSnapshot, Installation, Job, Tenant
from central.tests.test_control_plane import seed, setup_db
def _panel_client():
session_factory = setup_db()
agent_id, _ = seed(session_factory)
now = datetime(2026, 9, 22, 19, 11, tzinfo=timezone.utc)
with session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
agent = session.get(Agent, agent_id)
agent.agent_version = "0.9.0"
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.total_products = 50
snapshot.fiscally_complete = 49
snapshot.safe_blocked = 1
snapshot.failed_products = 0
snapshot.coverage_percent = 98
job = Job(
agent_id=agent_id,
kind="REPROCESS_PRODUCTS",
status="RUNNING",
created_at=now - timedelta(minutes=3),
started_at=now - timedelta(minutes=2),
payload={"codigos": ["P-001"]},
)
session.add(job)
session.flush()
blocker = upsert_blocker(
session,
installation.tenant_id,
{
"company": "Acme",
"product_code": "P-001",
"description": "Produto de teste",
"blocker_type": "BLOCKED_MISSING_EVIDENCE",
"field": "CFOP",
"reason_code": "MISSING_CONTEXT",
"input_fingerprint": "page-test",
"evidence_summary": {"source": "snapshot"},
},
installation_id=installation.id,
)
blocker.first_seen_at = now - timedelta(days=1, seconds=11)
blocker.last_seen_at = now.replace(second=37)
record_event(
session,
tenant_id=installation.tenant_id,
installation_id=installation.id,
agent_id=agent_id,
severity="WARNING",
event_type="PRODUCT_BLOCKED",
message="Contexto fiscal ausente",
job_id=job.id,
product_code="P-001",
blocker_id=blocker.id,
occurred_at=now,
)
session.commit()
client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None))
client._session_factory = session_factory
login = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
assert login.status_code == 303
return client, agent_id, blocker.id
def test_dashboard_renders_operational_queue_labels_and_human_dates():
client, _, _ = _panel_client()
response = client.get("/")
assert response.status_code == 200
for label in ("Agents total", "Online", "Degraded", "Offline", "Failed", "Unknown"):
assert label in response.text
assert "Produtos com blockers abertos" in response.text
assert "Blockers abertos" in response.text
assert "ATENÇÃO NECESSÁRIA" in response.text
assert "Ver todos" in response.text
assert "Jobs com falha" in response.text
assert "Agents desatualizados" in response.text
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", response.text)
def test_dashboard_coverage_displays_reconciled_product_buckets():
client, _, _ = _panel_client()
response = client.get("/")
assert response.status_code == 200
for label in ("Produtos avaliados", "Completos", "Bloqueados com segurança", "Falhas técnicas", "Não classificados"):
assert label in response.text
assert "Produtos com blockers abertos" in response.text
assert "Cobertura reconciliada" in response.text
def test_login_has_centered_card_and_explicit_field_labels():
client, _, _ = _panel_client()
response = client.get("/login")
assert response.status_code == 200
assert 'class="login-page"' in response.text
# Same card component as the GTI Gerenciador login (gti-theme.css .cartao).
assert 'class="cartao login-card"' in response.text
assert '<label for="login-username">Usuário</label>' in response.text
assert '<label for="login-password">Senha</label>' in response.text
def test_dashboard_lists_bounded_failed_job_rows_with_identity_error_age_and_empty_state():
client, agent_id, _ = _panel_client()
factory = client._session_factory
with factory() as session:
for index in range(12):
session.add(Job(agent_id=agent_id, kind="REPROCESS_PRODUCTS", status="FAILED", error=f"erro curto {index}", created_at=datetime(2026, 9, 22, 18, 0, index, tzinfo=timezone.utc), finished_at=datetime(2026, 9, 22, 18, 5, index, tzinfo=timezone.utc)))
session.commit()
response = client.get("/")
assert response.status_code == 200
assert "Acme" in response.text and "Acme ERP" in response.text and "srv-01" in response.text
assert "erro curto" in response.text and "Job ID" in response.text and "Criado" in response.text
assert 'href="/jobs?job_id=' in response.text
assert response.text.count("erro curto") == 10
assert 'title="2026-09-22T18:00:11+00:00"' in response.text
assert 'title="2026-09-22T18:05:11+00:00"' in response.text
assert ">22/09/2026 15:00<" in response.text
assert ">22/09/2026 15:05<" in response.text
with factory() as session:
session.query(Job).filter(Job.status == "FAILED").delete()
session.commit()
assert "Nenhum job com falha." in client.get("/").text
def test_blocker_summary_uses_complete_filtered_relation_and_tenant_product_identity():
client, agent_id, _ = _panel_client()
factory = client._session_factory
with factory() as session:
original = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
other_tenant = Tenant(name="Outra empresa")
session.add(other_tenant); session.flush()
other_agent = Agent(tenant_id=other_tenant.id, name="agent-2", token_hash="y" * 64)
session.add(other_agent); session.flush()
other_installation = Installation(agent_id=other_agent.id, tenant_id=other_tenant.id, installation_id="22222222-2222-4222-8222-222222222222", display_name="Outra ERP", hostname="srv-02")
session.add(other_installation); session.flush()
for index in range(55):
upsert_blocker(session, original.tenant_id, {"company": "Acme", "product_code": f"P-{index:03}", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": f"many-{index}"}, installation_id=original.id)
upsert_blocker(session, other_tenant.id, {"company": "Outra", "product_code": "P-000", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "NCM", "reason_code": "MISSING", "input_fingerprint": "other-tenant"}, installation_id=other_installation.id)
resolved = upsert_blocker(session, original.tenant_id, {"company": "Acme", "product_code": "P-RES", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "ST", "reason_code": "MISSING", "input_fingerprint": "resolved"}, installation_id=original.id)
resolved.resolved_at = datetime.now(timezone.utc)
session.commit()
open_page = client.get("/blockers?status=OPEN&field=CFOP")
assert open_page.status_code == 200
assert '<span>Blockers abertos</span><strong>56</strong>' in open_page.text
assert '<span>Produtos afetados</span><strong>55</strong>' in open_page.text
assert '<span>Clientes afetados</span><strong>1</strong>' in open_page.text
resolved_page = client.get("/blockers?status=RESOLVED")
assert '<span>Blockers abertos</span><strong>0</strong>' in resolved_page.text
assert '<span>Produtos afetados</span><strong>1</strong>' in resolved_page.text
all_page = client.get("/blockers?status=ALL")
assert '<span>Blockers abertos</span><strong>57</strong>' in all_page.text
assert '<span>Produtos afetados</span><strong>57</strong>' in all_page.text
assert "Produtos afetados" in open_page.text and "Clientes afetados" in open_page.text
def test_agents_render_separate_health_and_fiscal_status_with_server_sort():
client, _, _ = _panel_client()
response = client.get("/agents?sort=worst_coverage&health=ONLINE&search=Acme")
assert response.status_code == 200
for label in ("Cliente", "Nome de exibição", "Hostname", "Saúde técnica", "Status fiscal", "Cobertura fiscal", "Versão"):
assert label in response.text
assert "Acme ERP" in response.text
assert "srv-01" in response.text
assert "98,0%" in response.text
assert 'name="health"' in response.text
assert 'value="Acme"' in response.text
assert 'value="worst_coverage"' in response.text
def test_agent_list_hides_technical_uuid_and_explains_remainder():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
technical_uuid = installation.installation_id
agent = session.get(Agent, agent_id)
agent.agent_version = None
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.version_drift = "UNKNOWN"
session.commit()
response = client.get("/agents")
assert response.status_code == 200
assert technical_uuid not in response.text
assert "classificados" in response.text
assert "bloqueados com segurança" in response.text
assert "falhas técnicas" in response.text
assert "<td>Desconhecida<small>Desconhecida</small></td>" not in response.text
def test_agent_list_and_detail_show_localized_last_seen_with_iso_tooltip():
client, agent_id, _ = _panel_client()
expected_iso = '2026-09-22T19:11:37+00:00'
expected_display = '22/09/2026 16:11'
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
installation.last_seen_at = datetime(2026, 9, 22, 19, 11, 37, tzinfo=timezone.utc)
session.commit()
agents = client.get("/agents")
detail = client.get(f"/agents/{agent_id}")
assert agents.status_code == 200
assert f'title="{expected_iso}"' in agents.text
assert f'>{expected_display}<' in agents.text
assert detail.status_code == 200
assert f'title="{expected_iso}"' in detail.text
assert f'>{expected_display} ·' in detail.text
def test_jobs_render_client_identity_duration_and_preserve_filters():
client, _, _ = _panel_client()
response = client.get("/jobs?status=RUNNING&search=Acme")
assert response.status_code == 200
for label in ("Cliente", "Nome de exibição", "Hostname", "Job ID", "Tipo", "Status", "Criado", "Finalizado", "Duração", "Erro"):
assert label in response.text
assert "Acme" in response.text
assert "srv-01" in response.text
assert "em andamento" in response.text
assert 'value="Acme"' in response.text
assert '<option value="RUNNING" selected>' in response.text
assert "Agent 1" not in response.text
def test_jobs_show_localized_created_and_finished_with_iso_tooltips():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
session.add(Job(agent_id=agent_id, kind="REPROCESS_PRODUCTS", status="COMPLETED", created_at=datetime(2026, 9, 22, 19, 8, tzinfo=timezone.utc), started_at=datetime(2026, 9, 22, 19, 9, tzinfo=timezone.utc), finished_at=datetime(2026, 9, 22, 19, 10, tzinfo=timezone.utc)))
session.commit()
response = client.get("/jobs")
assert response.status_code == 200
assert 'title="2026-09-22T19:08:00+00:00"' in response.text
assert 'title="2026-09-22T19:10:00+00:00"' in response.text
assert ">22/09/2026 16:08<" in response.text
assert ">22/09/2026 16:10<" in response.text
def test_blocker_list_and_detail_show_localized_seen_times_with_iso_tooltips():
client, _, blocker_id = _panel_client()
listing = client.get("/blockers")
detail = client.get(f"/blockers/{blocker_id}")
assert listing.status_code == 200
assert 'title="2026-09-22T19:11:37+00:00"' in listing.text
assert ">22/09/2026 16:11<" in listing.text
assert detail.status_code == 200
assert 'title="2026-09-21T19:10:49+00:00"' in detail.text
assert 'title="2026-09-22T19:11:37+00:00"' in detail.text
assert ">22/09/2026 16:11<" in detail.text
def test_blockers_render_visible_filters_summary_priority_and_detail_link():
client, _, blocker_id = _panel_client()
response = client.get("/blockers?client=Acme&installation=srv-01&field=CFOP&blocker_type=BLOCKED_MISSING_EVIDENCE&status=OPEN&age=0h&search=P-001")
assert response.status_code == 200
for label in ("Cliente", "Instalação / Agent / Servidor", "Campo", "Tipo de blocker", "Status", "Idade", "Produto ou busca", "Blockers abertos", "Produtos afetados", "Clientes afetados", "CFOP", "ST", "ICMS", "CSOSN", "NCM", "OTHER", "Prioridade"):
assert label in response.text
assert f"/blockers/{blocker_id}" in response.text
assert "CRITICAL" in response.text or "HIGH" in response.text
assert 'value="Acme"' in response.text
assert 'value="BLOCKED_MISSING_EVIDENCE"' in response.text
assert "window.confirm" not in response.text
def test_agent_and_blocker_details_keep_safe_forms_and_human_event_rows():
client, agent_id, blocker_id = _panel_client()
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
upsert_blocker(session, installation.tenant_id, {"company": "Acme", "product_code": "LEGACY", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "ST", "reason_code": "LEGACY", "input_fingerprint": "tenant-only"})
session.commit()
agent_response = client.get(f"/agents/{agent_id}?tab=events&severity=WARNING&event_type=PRODUCT_BLOCKED&page=2")
blocker_response = client.get(f"/blockers/{blocker_id}")
assert agent_response.status_code == 200
for label in ("Cliente", "Nome de exibição", "Hostname", "Saúde técnica", "Status fiscal", "Último heartbeat", "Versão", "Empresa", "UF", "CRT", "Eventos recentes", "Severidade", "Tipo de evento"):
assert label in agent_response.text
assert "Contexto fiscal ausente" in agent_response.text
assert f"/blockers?agent_id={agent_id}" in agent_response.text
assert ">CFOP</span><b>1</b>" in agent_response.text
assert ">ST</span><b>0</b>" in agent_response.text
agent_blockers = client.get(f"/blockers?agent_id={agent_id}")
assert "Produto de teste" in agent_blockers.text and "LEGACY" not in agent_blockers.text
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", re.sub(r' title="[^"]*"', "", agent_response.text))
assert 'value="WARNING" selected' in agent_response.text
assert 'value="PRODUCT_BLOCKED"' in agent_response.text
assert blocker_response.status_code == 200
for label in ("Detalhes do blocker", "Evidência", "Contexto atual", "Salvar contexto e reprocessar", "Chave de contexto", "Valor", "Escopo", "Nota"):
assert label in blocker_response.text
assert 'name="context"' not in blocker_response.text
assert "data-confirm" not in blocker_response.text
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", re.sub(r' title="[^"]*"', "", blocker_response.text))
def test_legacy_blocker_detail_explains_unavailable_resolution_without_form():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
blocker = upsert_blocker(
session,
installation.tenant_id,
{"company": "Acme", "product_code": "LEGACY", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "ST", "reason_code": "LEGACY", "input_fingerprint": "unbound-detail"},
)
session.commit()
blocker_id = blocker.id
response = client.get(f"/blockers/{blocker_id}")
assert response.status_code == 200
assert "blocker legado de empresa" in response.text.lower()
assert "sem vínculo comprovado com um servidor" in response.text.lower()
assert "não é possível oferecer o reprocessamento de contexto com segurança" in response.text.lower()
assert "Salvar contexto e reprocessar" not in response.text
assert f'action="/blockers/{blocker_id}/resolve-context"' not in response.text
def test_agent_detail_resolves_agent_after_first_hundred_installations():
client, _, _ = _panel_client()
factory = client._session_factory
with factory() as session:
tenant = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
target_id = 0
for index in range(105):
agent = Agent(tenant_id=tenant.id, name=f"late-{index}", token_hash=f"{index:064d}")
session.add(agent); session.flush()
session.add(Installation(agent_id=agent.id, tenant_id=tenant.id, installation_id=f"{index + 10:08d}-1111-4111-8111-111111111111", display_name=f"Late ERP {index}", hostname=f"late-{index}"))
target_id = agent.id
session.commit()
response = client.get(f"/agents/{target_id}")
assert response.status_code == 200
assert "Late ERP 104" in response.text
def test_agent_detail_reconciles_coverage_and_formats_missing_technical_fields():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
agent = session.get(Agent, agent_id)
agent.agent_version = None
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.version_drift = "UNKNOWN"
session.commit()
response = client.get(f"/agents/{agent_id}")
assert response.status_code == 200
for label in ("Produtos", "Completos", "Bloqueados com segurança", "Falhas técnicas", "Não classificados", "Cobertura fiscal"):
assert label in response.text
assert "Não informado" in response.text
assert "Desconhecida" in response.text
assert "Desconhecida · Desconhecida" not in response.text
assert "ODS -" not in response.text
context = response.text.split("<h2>Contexto da empresa</h2>", 1)[1].split("</article>", 1)[0]
assert "Reprocessar tudo" in context
assert not re.search(r"<(?:a|button)\b[^>]*>\s*</(?:a|button)>", context)
def test_agent_detail_reports_negative_coverage_counts_as_data_inconsistency():
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
snapshot.failed_products = -1
session.commit()
response = client.get(f"/agents/{agent_id}")
assert response.status_code == 200
assert "contagens negativas" in response.text
def test_agent_event_filters_lookahead_and_safe_event_reference_links_and_confirmation():
client, agent_id, _ = _panel_client()
factory = client._session_factory
with factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
job = session.scalar(select(Job).where(Job.agent_id == agent_id))
for index in range(27):
session.add(AgentEvent(tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="INFO", event_type="HEARTBEAT", message=f"evento {index}", structured_context={}, job_id=job.id, product_code="P-001", occurred_at=datetime(2026, 9, 21 + index % 2, tzinfo=timezone.utc)))
session.add(AgentEvent(tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="INFO", event_type="HEARTBEAT", message="evento anterior ao filtro", structured_context={}, occurred_at=datetime(2026, 9, 20, tzinfo=timezone.utc)))
session.commit()
first = client.get(f"/agents/{agent_id}?tab=events&since=2026-09-21&until=2026-09-22")
assert first.status_code == 200
assert 'name="since"' in first.text and 'name="until"' in first.text
assert "evento anterior ao filtro" not in first.text
assert "Próxima" in first.text and "since=2026-09-21" in first.text and "until=2026-09-22" in first.text
last = client.get(f"/agents/{agent_id}?tab=events&event_page=2&since=2026-09-21&until=2026-09-22")
assert "Anterior" in last.text and "Próxima" not in last.text
assert "/jobs?job_id=" in first.text and "/blockers?agent_id=" in first.text
csrf = client.cookies.get("gti_csrf")
blocked = client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": csrf})
assert blocked.status_code == 400 and "confirmar" in blocked.text.lower() and 'name="confirm"' in blocked.text
confirmation = client.get(f"/agents/{agent_id}/reprocess-confirmation")
assert confirmation.status_code == 200 and 'name="confirm"' in confirmation.text and 'required' in confirmation.text
assert f'href="/agents/{agent_id}"' in confirmation.text
assert client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": "wrong", "confirm": "yes"}).status_code == 403
assert client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": csrf, "confirm": "yes"}, follow_redirects=False).status_code == 303