mirror of https://github.com/interlegis/sapl.git
Browse Source
* Verifica permissão de acesso em documentos administrativos Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Serve arquivos de mídia via Django com X-Accel-Redirect Todo /media/ passa pelo Django, que verifica a permissão dos arquivos de documentos administrativos e proposições e define Content-Type e Content-Disposition. O nginx entrega o arquivo por uma location interna. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Recusa upload de arquivos com conteúdo executável pelo navegador Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Oculta sessões não iniciadas também na API e nos arquivos de mídia Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Valida arquivo antes de gravar no cadastro de acessórios em lote Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Identifica conteúdo ativo também pelo tipo MIME da extensão Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Não redireciona arquivos de mídia para troca de senha Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ajusta regras de acesso a arquivos de documentos e proposições Documentos administrativos e acessórios ficam ocultos ao público quando a configuração é restritiva ou o documento é restrito. O texto original da proposição nunca é público. Arquivos de sessões plenárias são públicos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Aplica regras da página de detalhe e de sessões ocultas aos arquivos No modo restritivo, os arquivos de documentos administrativos exigem a mesma permissão da página de detalhe. Arquivos de sessões ocultas e de seus oradores e justificativas não são servidos ao público. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Usa permissões por estado no acesso ao texto original da proposição Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Centraliza validação de caminho dos arquivos de mídia Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ajusta teste de sessão oculta à regra de data da sessão Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Update sapl/materia/views.py Co-authored-by: joaohortsenado <100957576+joaohortsenado@users.noreply.github.com> * Update sapl/base/media.py Co-authored-by: joaohortsenado <100957576+joaohortsenado@users.noreply.github.com> * Update sapl/base/media.py Co-authored-by: joaohortsenado <100957576+joaohortsenado@users.noreply.github.com> * Exige permissão de proposição para receber e confirmar proposições Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Corrige redirecionamento do PDF de acessórios de documento administrativo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Usa a mesma configuração na página e nos arquivos de documento administrativo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Remove XML da lista de tipos de texto aceitos no upload O upload de XML já era recusado pela checagem de conteúdo ativo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Envia ao Django só os arquivos de mídia que dependem de autorização Os demais arquivos de /media/ voltam a ser servidos direto pelo nginx, com nosniff e download forçado para extensões ativas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Detecta extensões ativas sem depender de /etc/mime.types A imagem python:3.12-slim não tem /etc/mime.types, então mimetypes não reconhecia .xht, .xslt, .kml etc. e o upload/entrega deixavam passar. EXTENSOES_ATIVAS passa a cobrir as mesmas extensões do nginx (mais .kml e .xspf), com teste que mantém as duas listas iguais. media-types entra na imagem para o tipo_ativo reconhecer os demais +xml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Serve arquivos públicos só com tipos conhecidos Em vez de depender do mime.types global, que entrega tipos +xml (.kml, .xspf) inline, a location /media/ lista os tipos que podem abrir no navegador e manda o resto como application/octet-stream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Edward <9326037+edwardoliveira@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: joaohortsenado <100957576+joaohortsenado@users.noreply.github.com>pull/3867/head
committed by
GitHub
17 changed files with 1274 additions and 92 deletions
@ -0,0 +1,124 @@ |
|||||
|
import mimetypes |
||||
|
import os |
||||
|
import re |
||||
|
from urllib.parse import quote |
||||
|
|
||||
|
from django.conf import settings |
||||
|
from django.http import FileResponse, Http404, HttpResponse |
||||
|
|
||||
|
from sapl.materia.models import Proposicao, pode_ver_proposicao |
||||
|
from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, |
||||
|
DocumentoAdministrativo, |
||||
|
pode_ver_documento_adm) |
||||
|
from sapl.sessao.models import (JustificativaAusencia, Orador, |
||||
|
OradorExpediente, OradorOrdemDia, |
||||
|
SessaoPlenaria, restringe_sessoes_visiveis) |
||||
|
from sapl.utils import EXTENSOES_ATIVAS, tipo_ativo |
||||
|
|
||||
|
X_ACCEL_PREFIX = '/media-interna/' |
||||
|
|
||||
|
# O nginx do container só envia para cá os caminhos de PROTEGIDO_RE e de |
||||
|
# MODELOS_DE_SESSAO; o resto de /media/ sai direto do disco. Ao incluir um |
||||
|
# modelo aqui, incluí-lo também em docker/config/nginx/sapl.conf. |
||||
|
|
||||
|
# Arquivos que dependem de autorização, em private/ ou em qualquer outra |
||||
|
# pasta (public/ guarda os enviados antes da criação de private/). |
||||
|
PROTEGIDO_RE = re.compile( |
||||
|
r'^sapl/[^/]+/(?:documentoadministrativo|' |
||||
|
r'documentoacessorioadministrativo|proposicao)/') |
||||
|
|
||||
|
RESOLVEDORES = { |
||||
|
'documentoadministrativo': ( |
||||
|
DocumentoAdministrativo, 'texto_integral', pode_ver_documento_adm), |
||||
|
'documentoacessorioadministrativo': ( |
||||
|
DocumentoAcessorioAdministrativo, 'arquivo', |
||||
|
lambda user, obj: pode_ver_documento_adm(user, obj.documento)), |
||||
|
'proposicao': ( |
||||
|
Proposicao, 'texto_original', pode_ver_proposicao), |
||||
|
} |
||||
|
|
||||
|
|
||||
|
# Arquivos gravados em sapl/public/<model>/<pk>/..., que seguem a |
||||
|
# visibilidade da sessão plenária a que pertencem. |
||||
|
MODELOS_DE_SESSAO = {m._meta.model_name: m for m in ( |
||||
|
SessaoPlenaria, Orador, OradorExpediente, OradorOrdemDia, |
||||
|
JustificativaAusencia)} |
||||
|
|
||||
|
|
||||
|
def _de_sessao_oculta(request, partes): |
||||
|
model = MODELOS_DE_SESSAO.get(partes[2]) |
||||
|
if request.user.is_authenticated or not model or len(partes) < 5 or \ |
||||
|
partes[1] != 'public' or not re.fullmatch(r'[0-9]+', partes[3]): |
||||
|
return False |
||||
|
if model is SessaoPlenaria: |
||||
|
sessoes = SessaoPlenaria.objects.filter(pk=partes[3]) |
||||
|
else: |
||||
|
sessoes = SessaoPlenaria.objects.filter( |
||||
|
pk__in=model.objects.filter(pk=partes[3]).values( |
||||
|
'sessao_plenaria_id')) |
||||
|
return sessoes.exists() and not restringe_sessoes_visiveis( |
||||
|
sessoes, request.user).exists() |
||||
|
|
||||
|
|
||||
|
def _nome_protegido(request, path): |
||||
|
"""Devolve o nome do arquivo gravado no objeto dono, se o usuário puder |
||||
|
vê-lo. O caminho servido vem do banco, nunca da URL.""" |
||||
|
resolvedor = RESOLVEDORES.get(path.split('/')[2]) |
||||
|
if not resolvedor: |
||||
|
raise Http404 |
||||
|
model, campo, pode_ver = resolvedor |
||||
|
obj = model.objects.filter( |
||||
|
**{campo + '__in': [path, './' + path]}).first() |
||||
|
if not obj: |
||||
|
raise Http404 |
||||
|
arquivo = getattr(obj, campo) |
||||
|
if not arquivo or not pode_ver(request.user, obj): |
||||
|
raise Http404 |
||||
|
return arquivo.name[2:] if arquivo.name.startswith('./') else arquivo.name |
||||
|
|
||||
|
|
||||
|
def _caminho_em_media(nome): |
||||
|
raiz = os.path.realpath(settings.MEDIA_ROOT) |
||||
|
caminho = os.path.realpath(os.path.join(raiz, nome)) |
||||
|
if not caminho.startswith(raiz + os.sep): |
||||
|
raise Http404 |
||||
|
return caminho |
||||
|
|
||||
|
|
||||
|
def media(request, path): |
||||
|
partes = path.split('/') |
||||
|
if any(p in ('', '.', '..') for p in partes) or '\x00' in path: |
||||
|
raise Http404 |
||||
|
|
||||
|
protegido = path.startswith('sapl/private/') or bool( |
||||
|
PROTEGIDO_RE.match(path)) |
||||
|
nome = _nome_protegido(request, path) if protegido else path |
||||
|
|
||||
|
if len(partes) > 2 and _de_sessao_oculta(request, partes): |
||||
|
raise Http404 |
||||
|
nome_arquivo = os.path.basename(nome) |
||||
|
|
||||
|
content_type = (mimetypes.guess_type(nome_arquivo)[0] or |
||||
|
'application/octet-stream') |
||||
|
disposicao = 'inline' |
||||
|
if os.path.splitext(nome_arquivo)[1].lower() in EXTENSOES_ATIVAS or \ |
||||
|
tipo_ativo(content_type): |
||||
|
content_type, disposicao = 'application/octet-stream', 'attachment' |
||||
|
|
||||
|
caminho = _caminho_em_media(nome) |
||||
|
if settings.DEBUG: |
||||
|
try: |
||||
|
response = FileResponse(open(caminho, 'rb'), |
||||
|
content_type=content_type) |
||||
|
except (FileNotFoundError, IsADirectoryError): |
||||
|
raise Http404 |
||||
|
else: |
||||
|
response = HttpResponse(content_type=content_type) |
||||
|
response['X-Accel-Redirect'] = X_ACCEL_PREFIX + quote(nome) |
||||
|
|
||||
|
response['Content-Disposition'] = "%s; filename*=UTF-8''%s" % ( |
||||
|
disposicao, quote(nome_arquivo)) |
||||
|
response['X-Content-Type-Options'] = 'nosniff' |
||||
|
if protegido or (len(partes) > 2 and partes[2] in MODELOS_DE_SESSAO): |
||||
|
response['Cache-Control'] = 'private' |
||||
|
return response |
||||
@ -0,0 +1,384 @@ |
|||||
|
import os |
||||
|
import re |
||||
|
|
||||
|
from django.conf import settings |
||||
|
from django.contrib.auth import get_user_model |
||||
|
from django.contrib.auth.models import AnonymousUser |
||||
|
from django.contrib.auth.models import Permission |
||||
|
from django.contrib.contenttypes.models import ContentType |
||||
|
from django.core.cache import cache |
||||
|
from django.http import Http404 |
||||
|
from django.utils import timezone |
||||
|
from model_bakery import baker |
||||
|
import pytest |
||||
|
|
||||
|
from sapl.base.media import MODELOS_DE_SESSAO, RESOLVEDORES, media |
||||
|
from sapl.base.models import AppConfig, Autor |
||||
|
from sapl.materia.models import Proposicao |
||||
|
from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, |
||||
|
DocumentoAdministrativo) |
||||
|
from sapl.utils import EXTENSOES_ATIVAS |
||||
|
|
||||
|
DOC_PATH = 'sapl/private/documentoadministrativo/2026/1/texto.pdf' |
||||
|
ACESSORIO_PATH = 'sapl/public/documentoacessorioadministrativo/2026/1/a.pdf' |
||||
|
PROPOSICAO_PATH = 'sapl/private/proposicao/2026/1/proposicao.pdf' |
||||
|
|
||||
|
|
||||
|
@pytest.fixture(autouse=True) |
||||
|
def limpa_cache_appconfig(): |
||||
|
# o cache em arquivo sobrevive ao rollback do teste |
||||
|
cache.clear() |
||||
|
yield |
||||
|
cache.clear() |
||||
|
|
||||
|
|
||||
|
@pytest.fixture(autouse=True) |
||||
|
def sem_debug(settings): |
||||
|
settings.DEBUG = False |
||||
|
|
||||
|
|
||||
|
def _config(comportamento): |
||||
|
cache.clear() |
||||
|
AppConfig.objects.all().delete() |
||||
|
baker.make(AppConfig, documentos_administrativos=comportamento) |
||||
|
|
||||
|
|
||||
|
def _login(client, username, *perms): |
||||
|
user = baker.make(get_user_model(), username=username) |
||||
|
for perm in perms: |
||||
|
app_label, codename = perm.split('.') |
||||
|
perm_obj = Permission.objects.filter( |
||||
|
content_type__app_label=app_label, codename=codename).first() |
||||
|
if not perm_obj: |
||||
|
perm_obj = Permission.objects.create( |
||||
|
codename=codename, name=codename, |
||||
|
content_type=ContentType.objects.get_for_model( |
||||
|
DocumentoAdministrativo)) |
||||
|
user.user_permissions.add(perm_obj) |
||||
|
client.force_login(user) |
||||
|
return user |
||||
|
|
||||
|
|
||||
|
def _get(client, path): |
||||
|
return client.get('/media/' + path) |
||||
|
|
||||
|
|
||||
|
def _assert_servido(response, path): |
||||
|
assert response.status_code == 200 |
||||
|
assert response['X-Accel-Redirect'] == '/media-interna/' + path |
||||
|
assert response['X-Content-Type-Options'] == 'nosniff' |
||||
|
assert response['Cache-Control'] == 'private' |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('comportamento,restrito,perfil,pode_ver', [ |
||||
|
('O', False, None, True), |
||||
|
('O', True, None, False), |
||||
|
('O', True, 'logado', True), |
||||
|
('R', False, None, False), |
||||
|
('R', False, 'logado', False), |
||||
|
('R', True, None, False), |
||||
|
('R', True, 'logado', False), |
||||
|
('R', True, 'com_permissao', True), |
||||
|
]) |
||||
|
def test_documento_administrativo(client, comportamento, restrito, perfil, |
||||
|
pode_ver): |
||||
|
_config(comportamento) |
||||
|
baker.make(DocumentoAdministrativo, restrito=restrito, |
||||
|
texto_integral=DOC_PATH) |
||||
|
if perfil == 'logado': |
||||
|
_login(client, 'logado') |
||||
|
elif perfil == 'com_permissao': |
||||
|
_login(client, 'perm', |
||||
|
'protocoloadm.detail_documentoadministrativo') |
||||
|
|
||||
|
response = _get(client, DOC_PATH) |
||||
|
|
||||
|
if pode_ver: |
||||
|
_assert_servido(response, DOC_PATH) |
||||
|
assert response['Content-Type'] == 'application/pdf' |
||||
|
else: |
||||
|
assert response.status_code == 404 |
||||
|
assert not response.has_header('X-Accel-Redirect') |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('restrito,pode_ver', [(False, True), (True, False)]) |
||||
|
def test_documento_acessorio_segue_documento(client, restrito, pode_ver): |
||||
|
_config('O') |
||||
|
documento = baker.make(DocumentoAdministrativo, restrito=restrito) |
||||
|
baker.make(DocumentoAcessorioAdministrativo, documento=documento, |
||||
|
arquivo=ACESSORIO_PATH) |
||||
|
|
||||
|
response = _get(client, ACESSORIO_PATH) |
||||
|
|
||||
|
assert response.status_code == (200 if pode_ver else 404) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_caminho_legado_e_prefixo_ponto_barra(client): |
||||
|
_config('O') |
||||
|
legado = 'sapl/public/documentoadministrativo/2010/7/antigo.pdf' |
||||
|
baker.make(DocumentoAdministrativo, texto_integral=legado) |
||||
|
baker.make(DocumentoAdministrativo, texto_integral='./' + DOC_PATH) |
||||
|
|
||||
|
_assert_servido(_get(client, legado), legado) |
||||
|
_assert_servido(_get(client, DOC_PATH), DOC_PATH) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('path', [ |
||||
|
'sapl/private/documentoadministrativo/2026/1/inexistente.pdf', |
||||
|
'sapl/private/outromodelo/2026/1/texto.pdf', |
||||
|
'sapl/private/documentoadministrativo/../../public/x.pdf', |
||||
|
'sapl/private/documentoadministrativo/%2e%2e/x.pdf', |
||||
|
]) |
||||
|
def test_caminho_invalido(client, path): |
||||
|
_config('O') |
||||
|
baker.make(DocumentoAdministrativo, texto_integral=DOC_PATH) |
||||
|
|
||||
|
assert _get(client, path).status_code == 404 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_extensao_ativa_e_baixada(client): |
||||
|
_config('O') |
||||
|
path = 'sapl/private/documentoadministrativo/2026/1/x.html' |
||||
|
baker.make(DocumentoAdministrativo, texto_integral=path) |
||||
|
|
||||
|
response = _get(client, path) |
||||
|
|
||||
|
_assert_servido(response, path) |
||||
|
assert response['Content-Type'] == 'application/octet-stream' |
||||
|
assert response['Content-Disposition'].startswith('attachment;') |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_nome_nao_ascii_codificado(client): |
||||
|
_config('O') |
||||
|
path = 'sapl/private/documentoadministrativo/2026/1/ofício_nº1.pdf' |
||||
|
baker.make(DocumentoAdministrativo, texto_integral=path) |
||||
|
|
||||
|
response = _get(client, path) |
||||
|
|
||||
|
assert response.status_code == 200 |
||||
|
assert response['X-Accel-Redirect'] == ( |
||||
|
'/media-interna/sapl/private/documentoadministrativo/2026/1/' |
||||
|
'of%C3%ADcio_n%C2%BA1.pdf') |
||||
|
|
||||
|
|
||||
|
ESTADOS_PROPOSICAO = ('rascunho', 'enviada', 'devolvida', 'incorporada') |
||||
|
PERMS_DOCADM = ['protocoloadm.add_documentoadministrativo', |
||||
|
'protocoloadm.change_documentoadministrativo', |
||||
|
'protocoloadm.detail_documentoadministrativo'] |
||||
|
|
||||
|
|
||||
|
def _proposicao(estado): |
||||
|
autor = baker.make(Autor) |
||||
|
operador = baker.make(get_user_model(), username='operador') |
||||
|
autor.operadores.add(operador) |
||||
|
agora = timezone.now() |
||||
|
# mesmos campos gravados pelo envio, devolução e incorporação |
||||
|
baker.make(Proposicao, autor=autor, texto_original=PROPOSICAO_PATH, |
||||
|
data_envio=agora if estado in ('enviada', 'incorporada') |
||||
|
else None, |
||||
|
data_recebimento=agora if estado == 'incorporada' else None, |
||||
|
data_devolucao=agora if estado == 'devolvida' else None) |
||||
|
return operador |
||||
|
|
||||
|
|
||||
|
def _login_proposicao(client, perfil, operador): |
||||
|
if perfil == 'operador': |
||||
|
client.force_login(operador) |
||||
|
elif perfil == 'docadm': |
||||
|
user = _login(client, 'docadm', *PERMS_DOCADM) |
||||
|
assert user.has_module_perms('protocoloadm') |
||||
|
elif perfil == 'outro': |
||||
|
_login(client, 'outro') |
||||
|
elif perfil: |
||||
|
_login(client, perfil, 'materia.detail_proposicao_' + perfil) |
||||
|
|
||||
|
|
||||
|
def _pode_ver_proposicao(estado, perfil): |
||||
|
return perfil == 'operador' or perfil == estado != 'rascunho' |
||||
|
|
||||
|
|
||||
|
GRADE_PROPOSICAO = [ |
||||
|
(estado, perfil, _pode_ver_proposicao(estado, perfil)) |
||||
|
for estado in ESTADOS_PROPOSICAO |
||||
|
for perfil in (None, 'operador', 'outro', 'docadm', 'enviada', |
||||
|
'devolvida', 'incorporada')] |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('estado,perfil,pode_ver', GRADE_PROPOSICAO) |
||||
|
def test_proposicao(client, estado, perfil, pode_ver): |
||||
|
_login_proposicao(client, perfil, _proposicao(estado)) |
||||
|
|
||||
|
response = _get(client, PROPOSICAO_PATH) |
||||
|
|
||||
|
if pode_ver: |
||||
|
_assert_servido(response, PROPOSICAO_PATH) |
||||
|
else: |
||||
|
assert response.status_code == 404 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_arquivo_publico_passa_por_x_accel(client): |
||||
|
path = 'sapl/public/materialegislativa/2026/1/materia.pdf' |
||||
|
|
||||
|
response = _get(client, path) |
||||
|
|
||||
|
assert response.status_code == 200 |
||||
|
assert response['X-Accel-Redirect'] == '/media-interna/' + path |
||||
|
assert response['Content-Type'] == 'application/pdf' |
||||
|
assert response['X-Content-Type-Options'] == 'nosniff' |
||||
|
assert not response.has_header('Cache-Control') |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('nome', ['x.svg', 'x.html', 'x.XML', 'x.js', |
||||
|
'x.xht', 'x.rdf', 'x.xslt', 'x.kml']) |
||||
|
def test_arquivo_publico_ativo_e_baixado(client, nome): |
||||
|
response = _get(client, 'sapl/public/normajuridica/2026/1/' + nome) |
||||
|
|
||||
|
assert response['Content-Type'] == 'application/octet-stream' |
||||
|
assert response['Content-Disposition'].startswith('attachment;') |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('path', [ |
||||
|
'sapl/public/../private/documentoadministrativo/2026/1/texto.pdf', |
||||
|
'sapl//private/documentoadministrativo/2026/1/texto.pdf', |
||||
|
'sapl/./private/documentoadministrativo/2026/1/texto.pdf', |
||||
|
'sapl/public/normajuridica/', |
||||
|
]) |
||||
|
def test_caminho_nao_normalizado(client, path): |
||||
|
assert _get(client, path).status_code == 404 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_debug_serve_arquivo(rf, settings, tmp_path): |
||||
|
settings.DEBUG = True |
||||
|
settings.MEDIA_ROOT = str(tmp_path) |
||||
|
_config('R') |
||||
|
publico = tmp_path / 'sapl/public/normajuridica/1/n.pdf' |
||||
|
publico.parent.mkdir(parents=True) |
||||
|
publico.write_bytes(b'%PDF-1.4 norma') |
||||
|
baker.make(DocumentoAdministrativo, texto_integral=DOC_PATH) |
||||
|
|
||||
|
def chama(path): |
||||
|
request = rf.get('/media/' + path) |
||||
|
request.user = AnonymousUser() |
||||
|
return media(request, path) |
||||
|
|
||||
|
response = chama('sapl/public/normajuridica/1/n.pdf') |
||||
|
assert b''.join(response.streaming_content) == b'%PDF-1.4 norma' |
||||
|
assert not response.has_header('X-Accel-Redirect') |
||||
|
|
||||
|
for path in (DOC_PATH, 'sapl/public/normajuridica/1/nao.pdf'): |
||||
|
with pytest.raises(Http404): |
||||
|
chama(path) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('debug', [False, True]) |
||||
|
def test_link_simbolico_para_fora_de_media(rf, settings, tmp_path, debug): |
||||
|
settings.DEBUG = debug |
||||
|
media_root = tmp_path / 'media' |
||||
|
fora = tmp_path / 'fora' |
||||
|
(media_root / 'sapl/public').mkdir(parents=True) |
||||
|
fora.mkdir() |
||||
|
(fora / 'segredo.txt').write_bytes(b'segredo') |
||||
|
(media_root / 'sapl/public/link').symlink_to(fora) |
||||
|
settings.MEDIA_ROOT = str(media_root) |
||||
|
request = rf.get('/media/sapl/public/link/segredo.txt') |
||||
|
request.user = AnonymousUser() |
||||
|
|
||||
|
with pytest.raises(Http404): |
||||
|
media(request, 'sapl/public/link/segredo.txt') |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('path', ['/etc/passwd', 'sapl/public//etc/passwd']) |
||||
|
def test_caminho_absoluto(rf, path): |
||||
|
request = rf.get('/media/' + path) |
||||
|
request.user = AnonymousUser() |
||||
|
|
||||
|
with pytest.raises(Http404): |
||||
|
media(request, path) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_modelo_protegido_fora_de_private_e_public(client): |
||||
|
_config('O') |
||||
|
path = 'sapl/outra/documentoadministrativo/2026/1/texto.pdf' |
||||
|
baker.make(DocumentoAdministrativo, restrito=True, texto_integral=path) |
||||
|
|
||||
|
assert _get(client, path).status_code == 404 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_senha_fraca_nao_redireciona_media(client): |
||||
|
_login(client, 'fraca') |
||||
|
session = client.session |
||||
|
session['weak_password'] = True |
||||
|
session.save() |
||||
|
|
||||
|
response = _get(client, 'sapl/public/casalegislativa/logotipo.png') |
||||
|
|
||||
|
assert response.status_code == 200 |
||||
|
assert response.has_header('X-Accel-Redirect') |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('estado,perfil,pode_ver', GRADE_PROPOSICAO) |
||||
|
def test_proposicao_texto(client, settings, tmp_path, estado, perfil, |
||||
|
pode_ver): |
||||
|
settings.MEDIA_ROOT = str(tmp_path) |
||||
|
arquivo = tmp_path / PROPOSICAO_PATH |
||||
|
arquivo.parent.mkdir(parents=True) |
||||
|
arquivo.write_bytes(b'%PDF-1.4 proposicao') |
||||
|
_login_proposicao(client, perfil, _proposicao(estado)) |
||||
|
pk = Proposicao.objects.get(texto_original=PROPOSICAO_PATH).pk |
||||
|
|
||||
|
response = client.get('/proposicao/texto/%s' % pk) |
||||
|
|
||||
|
if pode_ver: |
||||
|
assert response.status_code == 200 |
||||
|
assert response.content == b'%PDF-1.4 proposicao' |
||||
|
else: |
||||
|
assert response.status_code == 302 |
||||
|
assert response['Location'] == '/proposicao/%s' % pk |
||||
|
|
||||
|
|
||||
|
def test_nginx_envia_ao_django_todos_os_caminhos_protegidos(): |
||||
|
caminho = os.path.join(settings.PROJECT_DIR, 'docker', 'config', |
||||
|
'nginx', 'sapl.conf') |
||||
|
with open(caminho) as f: |
||||
|
conf = f.read() |
||||
|
regex = re.search(r'location ~\*? \^/media/sapl/\(private/\|' |
||||
|
r'\[\^/\]\+/\(([a-z|]+)\)/\)', conf) |
||||
|
|
||||
|
assert regex, 'location de arquivos protegidos não encontrada' |
||||
|
no_nginx = set(regex.group(1).split('|')) |
||||
|
assert set(RESOLVEDORES) | set(MODELOS_DE_SESSAO) <= no_nginx |
||||
|
assert conf.index(regex.group(0)) < conf.index('location /media/ {') |
||||
|
|
||||
|
|
||||
|
def test_nginx_forca_download_das_extensoes_ativas(): |
||||
|
caminho = os.path.join(settings.PROJECT_DIR, 'docker', 'config', |
||||
|
'nginx', 'sapl.conf') |
||||
|
with open(caminho) as f: |
||||
|
conf = f.read() |
||||
|
regex = re.search(r'location ~\*? \^/media/\.\+\\\.\(([a-z?|]+)\)\$', |
||||
|
conf) |
||||
|
|
||||
|
assert regex, 'location de extensões ativas não encontrada' |
||||
|
no_nginx = set() |
||||
|
for ext in regex.group(1).split('|'): |
||||
|
if ext.endswith('?'): |
||||
|
no_nginx |= {ext[:-2], ext[:-1]} |
||||
|
else: |
||||
|
no_nginx.add(ext) |
||||
|
assert {'.' + e for e in no_nginx} == EXTENSOES_ATIVAS |
||||
@ -0,0 +1,64 @@ |
|||||
|
import os |
||||
|
|
||||
|
from django.core.files.uploadedfile import SimpleUploadedFile |
||||
|
from django.urls import reverse |
||||
|
from model_bakery import baker |
||||
|
import pytest |
||||
|
|
||||
|
from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa, |
||||
|
TipoDocumento) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def dirs(settings, tmp_path, monkeypatch): |
||||
|
media = tmp_path / 'media' |
||||
|
temp = tmp_path / 'temp' |
||||
|
media.mkdir() |
||||
|
temp.mkdir() |
||||
|
settings.MEDIA_ROOT = str(media) |
||||
|
monkeypatch.setattr('sapl.materia.views.MEDIA_ROOT', str(media)) |
||||
|
monkeypatch.setattr('sapl.materia.views.get_tempfile_dir', |
||||
|
lambda: str(temp)) |
||||
|
return media, temp |
||||
|
|
||||
|
|
||||
|
def _post(client, nome, conteudo): |
||||
|
materias = baker.make(MateriaLegislativa, _quantity=2) |
||||
|
tipo = baker.make(TipoDocumento, descricao='Parecer') |
||||
|
client.post(reverse('sapl.materia:acessorio_em_lote'), { |
||||
|
'materia_id': [m.pk for m in materias], |
||||
|
'tipo': tipo.descricao, |
||||
|
'nome': 'Parecer', |
||||
|
'autor': 'Comissão', |
||||
|
'data': '12/10/2026', |
||||
|
'ementa': 'Ementa', |
||||
|
'arquivo': SimpleUploadedFile(nome, conteudo), |
||||
|
}) |
||||
|
return materias |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_arquivo_invalido_nao_grava_nada(admin_client, dirs): |
||||
|
media, temp = dirs |
||||
|
|
||||
|
_post(admin_client, 'x.html', b'%PDF-1.4\n<html><script></script>') |
||||
|
|
||||
|
assert not DocumentoAcessorio.objects.exists() |
||||
|
assert os.listdir(media) == [] |
||||
|
assert os.listdir(temp) == [] |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_arquivo_valido_copiado_para_cada_materia(admin_client, dirs): |
||||
|
media, temp = dirs |
||||
|
|
||||
|
materias = _post(admin_client, 'parecer.pdf', b'%PDF-1.4\n%%EOF') |
||||
|
|
||||
|
docs = DocumentoAcessorio.objects.filter(materia__in=materias) |
||||
|
assert docs.count() == 2 |
||||
|
for doc in docs: |
||||
|
assert doc.arquivo.name == ( |
||||
|
'sapl/public/documentoacessorio/2026/%s/parecer.pdf' % doc.pk) |
||||
|
assert (media / doc.arquivo.name).read_bytes() == b'%PDF-1.4\n%%EOF' |
||||
|
assert os.listdir(media) == ['sapl'] |
||||
|
assert os.listdir(temp) == [] |
||||
@ -0,0 +1,98 @@ |
|||||
|
from django.contrib.auth import get_user_model |
||||
|
from django.contrib.auth.models import Permission |
||||
|
from django.contrib.contenttypes.models import ContentType |
||||
|
from django.urls import reverse |
||||
|
from django.utils import timezone |
||||
|
from model_bakery import baker |
||||
|
import pytest |
||||
|
|
||||
|
from sapl.base.models import AppConfig |
||||
|
from sapl.materia.models import (Proposicao, TipoMateriaLegislativa, |
||||
|
TipoProposicao) |
||||
|
from sapl.utils import SEPARADOR_HASH_PROPOSICAO, gerar_hash_arquivo |
||||
|
|
||||
|
PROPOSICAO_PATH = 'sapl/private/proposicao/2026/1/proposicao.pdf' |
||||
|
PERMS_DOCADM = ['add_documentoadministrativo', |
||||
|
'change_documentoadministrativo', |
||||
|
'detail_documentoadministrativo'] |
||||
|
|
||||
|
|
||||
|
def _login(client, app_label, *codenames): |
||||
|
user = baker.make(get_user_model(), username=app_label) |
||||
|
user.user_permissions.add(*Permission.objects.filter( |
||||
|
content_type__app_label=app_label, codename__in=codenames)) |
||||
|
client.force_login(user) |
||||
|
return user |
||||
|
|
||||
|
|
||||
|
def _login_docadm(client): |
||||
|
user = _login(client, 'protocoloadm', *PERMS_DOCADM) |
||||
|
assert user.has_module_perms('protocoloadm') |
||||
|
|
||||
|
|
||||
|
def _login_protocolo(client): |
||||
|
_login(client, 'materia', 'detail_proposicao_enviada') |
||||
|
|
||||
|
|
||||
|
def _proposicao_enviada(settings, tmp_path): |
||||
|
settings.MEDIA_ROOT = str(tmp_path) |
||||
|
baker.make(AppConfig) |
||||
|
arquivo = tmp_path / PROPOSICAO_PATH |
||||
|
arquivo.parent.mkdir(parents=True) |
||||
|
arquivo.write_bytes(b'%PDF-1.4 proposicao') |
||||
|
tipo = baker.make( |
||||
|
TipoProposicao, |
||||
|
content_type=ContentType.objects.get_for_model(TipoMateriaLegislativa), |
||||
|
object_id=baker.make(TipoMateriaLegislativa).pk) |
||||
|
proposicao = baker.make(Proposicao, tipo=tipo, |
||||
|
texto_original=PROPOSICAO_PATH, |
||||
|
data_envio=timezone.now()) |
||||
|
hasher = gerar_hash_arquivo(str(arquivo), str(proposicao.pk)) |
||||
|
return proposicao, hasher[1:].split(SEPARADOR_HASH_PROPOSICAO)[0] |
||||
|
|
||||
|
|
||||
|
def _url_confirmar(proposicao, hash_): |
||||
|
return reverse('sapl.materia:proposicao-confirmar', |
||||
|
kwargs={'hash': hash_, 'pk': proposicao.pk}) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_receber_exige_permissao_de_proposicao(client): |
||||
|
_login_docadm(client) |
||||
|
url = reverse('sapl.materia:receber-proposicao') |
||||
|
|
||||
|
assert client.get(url).status_code == 403 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_receber_operador_de_protocolo(client): |
||||
|
_login_protocolo(client) |
||||
|
url = reverse('sapl.materia:receber-proposicao') |
||||
|
|
||||
|
assert client.get(url).status_code == 200 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_confirmar_exige_permissao_de_proposicao(client, settings, tmp_path): |
||||
|
proposicao, hash_ = _proposicao_enviada(settings, tmp_path) |
||||
|
_login_docadm(client) |
||||
|
|
||||
|
response = client.get(_url_confirmar(proposicao, hash_)) |
||||
|
|
||||
|
assert response.status_code == 403 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_confirmar_operador_de_protocolo_ve_texto(client, settings, |
||||
|
tmp_path): |
||||
|
settings.DEBUG = False |
||||
|
proposicao, hash_ = _proposicao_enviada(settings, tmp_path) |
||||
|
_login_protocolo(client) |
||||
|
|
||||
|
response = client.get(_url_confirmar(proposicao, hash_)) |
||||
|
|
||||
|
assert response.status_code == 200 |
||||
|
assert proposicao.texto_original.url.encode() in response.content |
||||
|
texto = client.get(proposicao.texto_original.url) |
||||
|
assert texto.status_code == 200 |
||||
|
assert texto['X-Accel-Redirect'] == '/media-interna/' + PROPOSICAO_PATH |
||||
@ -0,0 +1,163 @@ |
|||||
|
from io import BytesIO |
||||
|
|
||||
|
from PyPDF4 import PdfFileWriter |
||||
|
from django.contrib.auth import get_user_model |
||||
|
from django.contrib.auth.models import Permission |
||||
|
from django.contrib.contenttypes.models import ContentType |
||||
|
from django.core.cache import cache |
||||
|
from django.core.files.uploadedfile import SimpleUploadedFile |
||||
|
from django.urls import reverse |
||||
|
from model_bakery import baker |
||||
|
import pytest |
||||
|
|
||||
|
from sapl.base.models import AppConfig |
||||
|
from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, |
||||
|
DocumentoAdministrativo) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture(autouse=True) |
||||
|
def limpa_cache_appconfig(): |
||||
|
# o cache em arquivo sobrevive ao rollback do teste |
||||
|
cache.clear() |
||||
|
yield |
||||
|
cache.clear() |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def media_tmp(settings, tmp_path): |
||||
|
settings.MEDIA_ROOT = str(tmp_path) |
||||
|
return tmp_path |
||||
|
|
||||
|
|
||||
|
def _config(comportamento): |
||||
|
cache.clear() |
||||
|
AppConfig.objects.all().delete() |
||||
|
baker.make(AppConfig, documentos_administrativos=comportamento) |
||||
|
|
||||
|
|
||||
|
def _documento(restrito): |
||||
|
documento = baker.make(DocumentoAdministrativo, restrito=restrito) |
||||
|
documento.texto_integral = SimpleUploadedFile( |
||||
|
'texto.pdf', b'%PDF-1.4 conteudo', content_type='application/pdf') |
||||
|
documento.save() |
||||
|
return documento |
||||
|
|
||||
|
|
||||
|
def _client(client, perfil): |
||||
|
if perfil == 'anonimo': |
||||
|
return client |
||||
|
user = baker.make(get_user_model(), username=perfil) |
||||
|
if perfil == 'com_permissao': |
||||
|
ct = ContentType.objects.get_for_model(DocumentoAdministrativo) |
||||
|
perm, _ = Permission.objects.get_or_create( |
||||
|
codename='detail_documentoadministrativo', content_type=ct, |
||||
|
defaults={'name': 'detail'}) |
||||
|
user.user_permissions.add(perm) |
||||
|
client.force_login(user) |
||||
|
return client |
||||
|
|
||||
|
|
||||
|
ACESSO = [ |
||||
|
# (comportamento, restrito, perfil, pode_ver) |
||||
|
('O', False, 'anonimo', True), |
||||
|
('O', False, 'sem_permissao', True), |
||||
|
('O', False, 'com_permissao', True), |
||||
|
('O', True, 'anonimo', False), |
||||
|
('O', True, 'sem_permissao', True), |
||||
|
('O', True, 'com_permissao', True), |
||||
|
('R', False, 'anonimo', False), |
||||
|
('R', False, 'sem_permissao', False), |
||||
|
('R', False, 'com_permissao', True), |
||||
|
('R', True, 'anonimo', False), |
||||
|
('R', True, 'sem_permissao', False), |
||||
|
('R', True, 'com_permissao', True), |
||||
|
] |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('comportamento,restrito,perfil,pode_ver', ACESSO) |
||||
|
def test_doc_texto_integral_acesso(client, media_tmp, comportamento, |
||||
|
restrito, perfil, pode_ver): |
||||
|
_config(comportamento) |
||||
|
documento = _documento(restrito) |
||||
|
client = _client(client, perfil) |
||||
|
|
||||
|
response = client.get(reverse('sapl.protocoloadm:doc_texto_integral', |
||||
|
kwargs={'pk': documento.pk})) |
||||
|
|
||||
|
assert response.status_code == (200 if pode_ver else 404) |
||||
|
|
||||
|
|
||||
|
def _pdf(): |
||||
|
writer = PdfFileWriter() |
||||
|
writer.addBlankPage(72, 72) |
||||
|
data = BytesIO() |
||||
|
writer.write(data) |
||||
|
return data.getvalue() |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('comportamento,restrito,perfil,pode_ver', ACESSO) |
||||
|
def test_pdf_docacessorios_acesso(client, media_tmp, monkeypatch, |
||||
|
comportamento, restrito, perfil, pode_ver): |
||||
|
# create_pdf_docacessorios usa o MEDIA_ROOT importado de sapl.settings |
||||
|
monkeypatch.setattr('sapl.protocoloadm.views.MEDIA_ROOT', str(media_tmp)) |
||||
|
_config(comportamento) |
||||
|
documento = baker.make(DocumentoAdministrativo, restrito=restrito) |
||||
|
acessorio = baker.make(DocumentoAcessorioAdministrativo, |
||||
|
documento=documento) |
||||
|
acessorio.arquivo = SimpleUploadedFile( |
||||
|
'acessorio.pdf', _pdf(), content_type='application/pdf') |
||||
|
acessorio.save() |
||||
|
client = _client(client, perfil) |
||||
|
|
||||
|
response = client.get(reverse('sapl.protocoloadm:merge_docacessorios', |
||||
|
kwargs={'pk': documento.pk})) |
||||
|
|
||||
|
if pode_ver: |
||||
|
assert response.status_code == 200 |
||||
|
assert response['Content-Type'] == 'application/pdf' |
||||
|
assert response.content.startswith(b'%PDF') |
||||
|
else: |
||||
|
assert response.status_code == 404 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_pdf_docacessorios_sem_acessorios_volta_para_lista(client): |
||||
|
_config('O') |
||||
|
documento = baker.make(DocumentoAdministrativo, restrito=False) |
||||
|
|
||||
|
response = client.get(reverse('sapl.protocoloadm:merge_docacessorios', |
||||
|
kwargs={'pk': documento.pk})) |
||||
|
|
||||
|
assert response.status_code == 302 |
||||
|
assert response['Location'] == reverse( |
||||
|
'sapl.protocoloadm:documentoacessorioadministrativo_list', |
||||
|
kwargs={'pk': documento.pk}) |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
@pytest.mark.parametrize('url_name', ['doc_texto_integral', |
||||
|
'merge_docacessorios']) |
||||
|
def test_docadm_pk_inexistente(client, url_name): |
||||
|
_config('O') |
||||
|
response = client.get(reverse('sapl.protocoloadm:' + url_name, |
||||
|
kwargs={'pk': 999999})) |
||||
|
assert response.status_code == 404 |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_detalhe_e_arquivo_usam_a_mesma_configuracao(client, media_tmp): |
||||
|
# com mais de uma linha em AppConfig, página e arquivo leem a mesma |
||||
|
_config('O') |
||||
|
baker.make(AppConfig, documentos_administrativos='R') |
||||
|
documento = _documento(False) |
||||
|
|
||||
|
detalhe = client.get(reverse( |
||||
|
'sapl.protocoloadm:documentoadministrativo_detail', |
||||
|
kwargs={'pk': documento.pk})) |
||||
|
texto = client.get(reverse('sapl.protocoloadm:doc_texto_integral', |
||||
|
kwargs={'pk': documento.pk})) |
||||
|
|
||||
|
assert detalhe.status_code == 302 |
||||
|
assert texto.status_code == 404 |
||||
@ -0,0 +1,94 @@ |
|||||
|
from datetime import timedelta |
||||
|
|
||||
|
from django.contrib.auth import get_user_model |
||||
|
from django.core.cache import cache |
||||
|
from django.utils import timezone |
||||
|
from model_bakery import baker |
||||
|
import pytest |
||||
|
|
||||
|
from sapl.sessao.models import Orador, SessaoPlenaria |
||||
|
|
||||
|
|
||||
|
@pytest.fixture(autouse=True) |
||||
|
def limpa_cache_appconfig(): |
||||
|
# o cache em arquivo sobrevive ao rollback do teste |
||||
|
cache.clear() |
||||
|
yield |
||||
|
cache.clear() |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def sessoes(): |
||||
|
# só a sessão futura, não iniciada e sem pauta fica oculta do público |
||||
|
previa = baker.make(SessaoPlenaria, iniciada=False, publicar_pauta=False, |
||||
|
data_inicio=timezone.localdate() + timedelta(days=7)) |
||||
|
iniciada = baker.make(SessaoPlenaria, iniciada=True, publicar_pauta=False) |
||||
|
baker.make(Orador, sessao_plenaria=previa) |
||||
|
baker.make(Orador, sessao_plenaria=iniciada) |
||||
|
return previa, iniciada |
||||
|
|
||||
|
|
||||
|
def _listados(client, endpoint, campo, sessoes): |
||||
|
"""Sessões de `sessoes` referenciadas na listagem completa do endpoint.""" |
||||
|
valores, page = set(), 1 |
||||
|
while page: |
||||
|
response = client.get('/api/sessao/%s/?page_size=100&page=%s' % ( |
||||
|
endpoint, page)) |
||||
|
assert response.status_code == 200 |
||||
|
dados = response.json() |
||||
|
valores |= {r[campo] for r in dados['results']} |
||||
|
page = dados['pagination']['next_page'] |
||||
|
return valores & {s.pk for s in sessoes} |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_api_oculta_sessao_previa_do_anonimo(client, sessoes): |
||||
|
previa, iniciada = sessoes |
||||
|
|
||||
|
assert _listados(client, 'sessaoplenaria', 'id', sessoes) == { |
||||
|
iniciada.pk} |
||||
|
|
||||
|
assert client.get('/api/sessao/sessaoplenaria/%s/' % previa.pk |
||||
|
).status_code == 404 |
||||
|
assert client.get('/api/sessao/sessaoplenaria/%s/ecidadania/' % previa.pk |
||||
|
).status_code == 404 |
||||
|
assert client.get('/api/sessao/sessaoplenaria/%s/expedientes/' % previa.pk |
||||
|
).status_code == 404 |
||||
|
assert client.get('/api/sessao/sessaoplenaria/%s/expedientes/' |
||||
|
% iniciada.pk).status_code == 200 |
||||
|
|
||||
|
assert _listados(client, 'orador', 'sessao_plenaria', sessoes) == { |
||||
|
iniciada.pk} |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_api_mostra_sessao_previa_ao_usuario_autenticado(client, sessoes): |
||||
|
previa, iniciada = sessoes |
||||
|
client.force_login(baker.make(get_user_model())) |
||||
|
|
||||
|
assert _listados(client, 'sessaoplenaria', 'id', sessoes) == { |
||||
|
previa.pk, iniciada.pk} |
||||
|
assert client.get('/api/sessao/sessaoplenaria/%s/' % previa.pk |
||||
|
).status_code == 200 |
||||
|
assert _listados(client, 'orador', 'sessao_plenaria', sessoes) == { |
||||
|
previa.pk, iniciada.pk} |
||||
|
|
||||
|
|
||||
|
@pytest.mark.django_db(transaction=False) |
||||
|
def test_media_de_sessao_oculta_do_anonimo(client, settings, sessoes): |
||||
|
settings.DEBUG = False |
||||
|
previa, iniciada = sessoes |
||||
|
orador_previa = Orador.objects.get(sessao_plenaria=previa) |
||||
|
orador_iniciada = Orador.objects.get(sessao_plenaria=iniciada) |
||||
|
|
||||
|
def get(path): |
||||
|
return client.get('/media/sapl/public/' + path).status_code |
||||
|
|
||||
|
assert get('sessaoplenaria/%s/pauta/p.pdf' % previa.pk) == 404 |
||||
|
assert get('sessaoplenaria/%s/pauta/p.pdf' % iniciada.pk) == 200 |
||||
|
assert get('orador/%s/anexo/a.pdf' % orador_previa.pk) == 404 |
||||
|
assert get('orador/%s/anexo/a.pdf' % orador_iniciada.pk) == 200 |
||||
|
|
||||
|
client.force_login(baker.make(get_user_model())) |
||||
|
assert get('sessaoplenaria/%s/pauta/p.pdf' % previa.pk) == 200 |
||||
|
assert get('orador/%s/anexo/a.pdf' % orador_previa.pk) == 200 |
||||
Loading…
Reference in new issue