140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
"""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"
|