Browse Source

Fix(Assinatura): materializacao recusa rodar sem base URL em vez de falhar 861 vezes AB#1473 (#17)

Achado num acervo real em 22/08/2026: 0 de 861 materias DOCX tinham PDF-alvo, e
o motivo nao aparecia em lugar nenhum. So os 7 PDF passavam — PDF nao converte,
copia bytes — e davam a impressao de que a rotina estava viva.

A conversao DOCX->PDF e o OnlyOffice quem faz, e para isso ele BAIXA o documento
de origem por URL absoluta. `build_onlyoffice_url` prefere SAPL_INTERNAL_URL e so
cai no request sem ela; no cron o "request" e o `_RequisicaoDeSistema`, que monta
a partir de SITE_URL. Com as duas vazias a URL sai SEM HOST, o OnlyOffice
responde erro e cada materia vira um logger.error individual. O resumo da passada
(`0 gerados, 873 falhas`) nao se distingue de um dia normal no meio do log.

Duas mudancas, as duas sobre visibilidade:

1. Erro de CONFIGURACAO morre no comeco, alto: sem SAPL_INTERNAL_URL nem
   SITE_URL o comando levanta CommandError antes de varrer o acervo, dizendo
   qual variavel falta e por que. Vale para a passada unica e para o laco.

2. Passada que falha sem NENHUM gerado grita no stderr: falha em todas nao e
   documento podre avulso, e ambiente parado (OnlyOffice fora, URL que ele nao
   alcanca, MEDIA sem os binarios).

A fixture `base_url_configurada` entra em autouse no test_materializacao: base
URL e pressuposto de toda materializacao, nao caso de borda — a ausencia dela tem
os seus proprios testes.

Testes: 64 passed em sapl/integracao_hub/tests/ (eram 61).
pull/3858/head
Kemuel-sepulvida 4 weeks ago
committed by GitHub
parent
commit
2ac550b28a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 44
      sapl/integracao_hub/management/commands/materializar_pdfs_para_assinatura.py
  2. 70
      sapl/integracao_hub/tests/test_materializacao.py

44
sapl/integracao_hub/management/commands/materializar_pdfs_para_assinatura.py

@ -5,7 +5,7 @@ import time
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from sapl.integracao_hub.models import DocumentoParaAssinatura
@ -34,6 +34,21 @@ class _RequisicaoDeSistema:
return base.rstrip('/') + caminho
def _base_url_de_sistema():
"""URL que o OnlyOffice usa para BAIXAR o documento de origem, no caminho cron.
`build_onlyoffice_url` prefere `SAPL_INTERNAL_URL` e so cai no request sem
ela e no cron o "request" e o `_RequisicaoDeSistema`, que monta a partir de
`SITE_URL`. Sem nenhuma das duas, a URL sai SEM HOST, o OnlyOffice nao
consegue baixar e TODA materia DOCX falha na conversao. Descoberto em
22/08/2026 num acervo real: 0 de 861 DOCX materializaram, cada uma virando um
logger.error que ninguem le, enquanto os 7 PDF passavam (PDF nao converte, so
copia bytes) e davam a impressao de que a rotina estava viva.
"""
return (getattr(settings, 'SAPL_INTERNAL_URL', '')
or getattr(settings, 'SITE_URL', '') or '')
class Command(BaseCommand):
help = ('Materializa o PDF-alvo da assinatura (refinamento §5/§5.1): varre '
'matérias protocoladas com texto_original, gera o PDF uma única '
@ -54,6 +69,18 @@ class Command(BaseCommand):
'vira pendencia no app, em silencio.'))
def handle(self, *args, **options):
# Erro de CONFIGURACAO morre aqui, alto e cedo — nao vira 861 falhas por
# materia num log que ninguem le. Vale para a passada unica e para o laco:
# o container que sobe sem isso nunca materializa nada.
if not _base_url_de_sistema():
raise CommandError(
'materializar_pdfs: nem SAPL_INTERNAL_URL nem SITE_URL estao '
'configuradas. A conversao DOCX->PDF passa pelo OnlyOffice, que '
'BAIXA o documento de origem por URL absoluta — sem host ele '
'responde erro e NENHUMA materia DOCX vira pendencia de '
'assinatura. Configure SAPL_INTERNAL_URL com uma URL deste SAPL '
'que o servidor do OnlyOffice alcance.')
intervalo = options['intervalo']
if intervalo <= 0:
self._passada()
@ -99,6 +126,21 @@ class Command(BaseCommand):
'materializar_pdfs: %s gerados, %s retificados, %s em dia, '
'%s falhas' % (gerados, retificados, pulados, falhas))
# Falha sem NENHUM avanco nao e materia podre avulsa: e o ambiente
# inteiro parado (OnlyOffice fora do ar, URL que ele nao alcanca, MEDIA
# sem os binarios). Some do log comum porque cada materia falha
# individualmente e o resumo parece so mais uma linha de rotina.
if falhas and not gerados and not retificados:
aviso = (
'materializar_pdfs: %s falhas e NENHUM PDF-alvo gerado — isso e '
'ambiente, nao documento. Confira o OnlyOffice em %s e se ele '
'alcanca %s; enquanto isso nenhuma materia DOCX vira pendencia '
'de assinatura no app.' % (
falhas, getattr(settings, 'ONLYOFFICE_URL', '<ausente>'),
_base_url_de_sistema()))
self.stderr.write(aviso)
logger.error(aviso)
def _materializar(self, materia):
materia.texto_original.open('rb')
try:

70
sapl/integracao_hub/tests/test_materializacao.py

@ -3,6 +3,7 @@ import hashlib
import pytest
from django.core.files.base import ContentFile
from django.core.management import call_command
from django.core.management.base import CommandError
from django.utils import timezone
from model_bakery import baker
@ -13,6 +14,17 @@ from sapl.materia.models import MateriaLegislativa
PDF = b'%PDF-1.4 conteudo-original'
@pytest.fixture(autouse=True)
def base_url_configurada(settings):
"""Toda materializacao pressupoe uma base URL — o comando recusa rodar sem.
Fica em autouse porque a ausencia dela nao e um caso de borda a exercitar em
cada teste: e erro de ambiente, e tem os seus proprios dois testes abaixo.
"""
settings.SAPL_INTERNAL_URL = 'http://sapl-interno:8000'
settings.SITE_URL = ''
def criar_materia(protocolo=100, conteudo=PDF, nome='texto.pdf'):
materia = baker.make(MateriaLegislativa, numero_protocolo=protocolo)
if conteudo is not None:
@ -187,3 +199,61 @@ def test_laco_sobrevive_a_passada_que_estoura(db, monkeypatch):
call_command('materializar_pdfs_para_assinatura', intervalo=5)
assert len(passadas) == 2, 'o laço deve seguir apos a passada que estourou'
# ---------------------------------------------------------------------------
# Guarda de configuração: sem base URL o OnlyOffice não baixa nada
# ---------------------------------------------------------------------------
@pytest.mark.django_db(transaction=False)
def test_recusa_rodar_sem_nenhuma_base_url(db, settings):
"""O modo de falha que custou o acervo de Franco (22/08/2026).
Sem SAPL_INTERNAL_URL nem SITE_URL a URL de download sai sem host, o
OnlyOffice responde erro e TODA matéria DOCX falha na conversão 861 delas,
cada uma como um `logger.error` que ninguém , enquanto os poucos PDF
passavam e davam a impressão de rotina viva. Erro de ambiente morre no
começo, não em 861 falhas por documento.
"""
settings.SAPL_INTERNAL_URL = ''
settings.SITE_URL = ''
materia = criar_materia()
with pytest.raises(CommandError) as erro:
call_command('materializar_pdfs_para_assinatura')
assert 'SAPL_INTERNAL_URL' in str(erro.value)
assert not DocumentoParaAssinatura.objects.filter(materia=materia).exists()
@pytest.mark.django_db(transaction=False)
def test_site_url_sozinha_basta(db, settings):
"""SITE_URL é o fallback legítimo — a guarda pega ausência das DUAS."""
settings.SAPL_INTERNAL_URL = ''
settings.SITE_URL = 'https://sapl.exemplo.gov.br'
materia = criar_materia()
call_command('materializar_pdfs_para_assinatura')
assert DocumentoParaAssinatura.objects.filter(materia=materia).exists()
@pytest.mark.django_db(transaction=False)
def test_passada_sem_nenhum_avanco_grita_no_stderr(db, monkeypatch, capsys):
"""Falha sem nenhum gerado é ambiente parado, não documento podre avulso.
É o resumo que passava por linha de rotina: `0 gerados, 873 falhas` não se
distingue de um dia normal no meio do log.
"""
criar_materia(protocolo=201, nome='a.docx', conteudo=b'docx')
criar_materia(protocolo=202, nome='b.docx', conteudo=b'docx')
monkeypatch.setattr(
'sapl.materia.views_assinatura._gerar_pdf_da_materia',
lambda materia, request: (None, 'OnlyOffice: codigo -8'))
call_command('materializar_pdfs_para_assinatura')
erro = capsys.readouterr().err
assert 'NENHUM PDF-alvo gerado' in erro
assert 'e ambiente, nao documento' in erro

Loading…
Cancel
Save