feat(missao-7): bloco D - endurecimento HTTP, cobertura de testes e runbook de disaster recovery

This commit is contained in:
gti
2026-09-28 20:16:32 -03:00
parent 5510fdff60
commit 0c10ddd761
4 changed files with 311 additions and 41 deletions
+12 -1
View File
@@ -4,7 +4,7 @@ import logging
import sys
from collections.abc import Callable
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from .config import settings
from .db import SessionLocal, database_is_available
@@ -24,6 +24,17 @@ def create_app(database_probe: Callable[[], bool] = database_is_available, sessi
raise RuntimeError("AGENT_DEV_TOKEN must not be set when APP_ENV=production (register real Agents with central/scripts/register_agent.py)")
logger.info("starting GTI Fiscal Hub (environment=%s)", settings.environment)
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
if getattr(settings, "web_cookie_secure", False):
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
@app.get("/health")
def health() -> dict[str, str]:
if not database_probe():
+139
View File
@@ -0,0 +1,139 @@
"""Missão 7 — Endurecimento e testes de cobertura dos módulos fiscais."""
from datetime import datetime, timezone
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from fastapi.testclient import TestClient
from central.app.models import Base, Dataset
from central.app.reference_registry import (
register_draft, approve_draft, reject_draft, list_registry,
diff_against_vigente, technical_checklist, RegistryError
)
from central.app.classification import (
Classifier, classify_tipi_rate, description_tokens, normalize_description
)
from central.app.main import create_app
@pytest.fixture
def memory_db():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
yield session
def test_reference_registry_lifecycle_and_approval_workflow(memory_db):
"""Testa o ciclo de vida completo do Reference Registry: registro, diff, checklist, aprovação e rejeição."""
# 1. Registrar versão 1
d1 = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="a" * 64,
version_label="v1",
record_count=15100,
act="Resolução Gecex 2026",
)
assert d1.status == "DRAFT"
# Idempotência de registro
d1_again = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="a" * 64,
version_label="v1",
)
assert d1_again.id == d1.id
# Checklist técnico
chk = technical_checklist(memory_db, d1)
assert chk["ready_to_approve"] is True
assert chk["volume"]["passed"] is True
# Aprovar versão 1 -> VIGENTE
approved = approve_draft(memory_db, d1.id, admin_name="Operador Teste")
assert approved.status == "VIGENTE"
assert approved.approved_by == "Operador Teste"
# 2. Registrar versão 2
d2 = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="b" * 64,
version_label="v2",
record_count=15200,
)
diff = diff_against_vigente(memory_db, d2)
assert diff["is_first_version"] is False
assert diff["record_count_delta"] == 100
# Aprovar versão 2 -> Torna d1 OBSOLETO
approve_draft(memory_db, d2.id, admin_name="Admin 2")
memory_db.refresh(d1)
assert d1.status == "OBSOLETO"
assert d2.status == "VIGENTE"
# Rejeitar draft com erro
d3 = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="c" * 64,
version_label="v3",
parse_errors={"line_1": "invalid format"},
)
# Não pode aprovar draft com erros de parse
with pytest.raises(RegistryError, match="recorded parse errors"):
approve_draft(memory_db, d3.id, admin_name="Admin")
# Rejeitar draft
rejected = reject_draft(memory_db, d3.id, admin_name="Admin")
assert rejected.status == "REJECTED"
# Listar registry
items = list_registry(memory_db)
assert len(items) == 3
def test_classification_tipi_rate_classification_and_tokens():
"""Testa classificação determinística de taxas TIPI e tratamento de stopwords."""
class MockEntry:
def __init__(self, ex=None, text=None, rate=None):
self.ex = ex
self.ipi_rate_text = text
self.ipi_rate = rate
assert classify_tipi_rate(MockEntry(ex="01")) == "EX_SPECIFIC"
assert classify_tipi_rate(MockEntry(text=None)) == "SOURCE_BLANK"
assert classify_tipi_rate(MockEntry(text="NT")) == "NT_OR_NON_TAXED"
assert classify_tipi_rate(MockEntry(text="NÃO TRIBUTADO")) == "NT_OR_NON_TAXED"
assert classify_tipi_rate(MockEntry(rate=0, text="0")) == "EXPLICIT_ZERO"
assert classify_tipi_rate(MockEntry(rate=15.0, text="15%")) == "OTHER"
# Stopwords
tokens = description_tokens("CERVEJA DE MALTE COM LUPULO E CEVADA")
assert "DE" not in tokens
assert "COM" not in tokens
assert "E" not in tokens
assert "CERVEJA" in tokens
assert "MALTE" in tokens
def test_http_security_headers_middleware():
"""Testa se o middleware de cabeçalhos de segurança HTTP adiciona os headers corretos."""
app = create_app(database_probe=lambda: True, bootstrap_token="test-token")
client = TestClient(app)
response = client.get("/health")
assert response.status_code == 200
assert response.headers.get("X-Content-Type-Options") == "nosniff"
assert response.headers.get("X-Frame-Options") == "SAMEORIGIN"
assert response.headers.get("X-XSS-Protection") == "1; mode=block"
assert response.headers.get("Referrer-Policy") == "strict-origin-when-cross-origin"