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.conf import settings
from django.core.files.base import ContentFile 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 django.db import transaction
from sapl.integracao_hub.models import DocumentoParaAssinatura from sapl.integracao_hub.models import DocumentoParaAssinatura
@ -34,6 +34,21 @@ class _RequisicaoDeSistema:
return base.rstrip('/') + caminho 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): class Command(BaseCommand):
help = ('Materializa o PDF-alvo da assinatura (refinamento §5/§5.1): varre ' help = ('Materializa o PDF-alvo da assinatura (refinamento §5/§5.1): varre '
'matérias protocoladas com texto_original, gera o PDF uma única ' 'matérias protocoladas com texto_original, gera o PDF uma única '
@ -54,6 +69,18 @@ class Command(BaseCommand):
'vira pendencia no app, em silencio.')) 'vira pendencia no app, em silencio.'))
def handle(self, *args, **options): 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'] intervalo = options['intervalo']
if intervalo <= 0: if intervalo <= 0:
self._passada() self._passada()
@ -99,6 +126,21 @@ class Command(BaseCommand):
'materializar_pdfs: %s gerados, %s retificados, %s em dia, ' 'materializar_pdfs: %s gerados, %s retificados, %s em dia, '
'%s falhas' % (gerados, retificados, pulados, falhas)) '%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): def _materializar(self, materia):
materia.texto_original.open('rb') materia.texto_original.open('rb')
try: try:

70
sapl/integracao_hub/tests/test_materializacao.py

@ -3,6 +3,7 @@ import hashlib
import pytest import pytest
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import CommandError
from django.utils import timezone from django.utils import timezone
from model_bakery import baker from model_bakery import baker
@ -13,6 +14,17 @@ from sapl.materia.models import MateriaLegislativa
PDF = b'%PDF-1.4 conteudo-original' 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'): def criar_materia(protocolo=100, conteudo=PDF, nome='texto.pdf'):
materia = baker.make(MateriaLegislativa, numero_protocolo=protocolo) materia = baker.make(MateriaLegislativa, numero_protocolo=protocolo)
if conteudo is not None: 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) call_command('materializar_pdfs_para_assinatura', intervalo=5)
assert len(passadas) == 2, 'o laço deve seguir apos a passada que estourou' 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