mirror of https://github.com/interlegis/sapl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
15 KiB
412 lines
15 KiB
import pytest
|
|
from django.db import IntegrityError
|
|
from django.urls import reverse
|
|
from model_bakery import baker
|
|
|
|
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
|
|
from sapl.parlamentares.models import (Legislatura, Parlamentar,
|
|
SessaoLegislativa)
|
|
from sapl.sessao.models import (OrdemDia, PresencaOrdemDia, RegistroVotacao,
|
|
SessaoPlenaria, TipoResultadoVotacao,
|
|
TipoSessaoPlenaria, VotoParlamentar)
|
|
|
|
NOMINAL = 2
|
|
|
|
|
|
def _sessao_plenaria():
|
|
legislatura = baker.make(Legislatura)
|
|
sessao_legislativa = baker.make(SessaoLegislativa)
|
|
tipo = baker.make(TipoSessaoPlenaria)
|
|
return baker.make(SessaoPlenaria, legislatura=legislatura,
|
|
sessao_legislativa=sessao_legislativa, tipo=tipo, numero=1)
|
|
|
|
|
|
def _materia():
|
|
tipo_materia = baker.make(TipoMateriaLegislativa)
|
|
return baker.make(MateriaLegislativa, tipo=tipo_materia)
|
|
|
|
|
|
def _ordem_nominal_aberta():
|
|
sessao = _sessao_plenaria()
|
|
materia = _materia()
|
|
ordem = baker.make(OrdemDia, sessao_plenaria=sessao, materia=materia,
|
|
tipo_votacao=NOMINAL, votacao_aberta=True,
|
|
registro_aberto=False)
|
|
return sessao, ordem
|
|
|
|
|
|
def _presente(sessao):
|
|
parlamentar = baker.make(Parlamentar, ativo=True)
|
|
baker.make(PresencaOrdemDia, sessao_plenaria=sessao, parlamentar=parlamentar)
|
|
return parlamentar
|
|
|
|
|
|
def _registrar_url(sessao, ordem):
|
|
return reverse('sapl.sessao:votacaonominal',
|
|
kwargs={'pk': sessao.pk, 'oid': ordem.pk, 'mid': ordem.materia_id})
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_get_registrar_votacao_nao_bloqueia_novos_votos(admin_client):
|
|
"""
|
|
Regressão da causa raiz #1: abrir a tela "Registrar Votação" não pode,
|
|
sozinho, impedir que vereadores que ainda não votaram continuem votando.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
_presente(sessao)
|
|
|
|
response = admin_client.get(_registrar_url(sessao, ordem))
|
|
|
|
assert response.status_code == 200
|
|
ordem.refresh_from_db()
|
|
assert ordem.registro_aberto is False
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_post_sem_acao_reconhecida_apenas_renderiza(admin_client):
|
|
"""
|
|
O botão "Registrar Votação" agora faz POST (para evitar cache/replay de
|
|
GET), mas sem nenhuma chave de ação reconhecida isso deve continuar
|
|
sendo pura navegação, sem nenhum efeito colateral.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
_presente(sessao)
|
|
|
|
response = admin_client.post(_registrar_url(sessao, ordem), {})
|
|
|
|
assert response.status_code == 200
|
|
ordem.refresh_from_db()
|
|
assert ordem.registro_aberto is False
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_bloquear_e_reabrir_votacao_nao_mexe_em_votos_existentes(admin_client):
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
votante = _presente(sessao)
|
|
baker.make(VotoParlamentar, ordem=ordem, parlamentar=votante, voto='Sim')
|
|
|
|
url = _registrar_url(sessao, ordem)
|
|
|
|
response = admin_client.post(url, {'bloquear-registro-votacao': '1'})
|
|
assert response.status_code == 302
|
|
ordem.refresh_from_db()
|
|
assert ordem.registro_aberto is True
|
|
|
|
response = admin_client.post(url, {'reabrir-votacao': '1'})
|
|
assert response.status_code == 302
|
|
ordem.refresh_from_db()
|
|
assert ordem.registro_aberto is False
|
|
|
|
voto = VotoParlamentar.objects.get(ordem=ordem, parlamentar=votante)
|
|
assert voto.voto == 'Sim'
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_salvar_votacao_nao_sobrescreve_voto_ja_registrado(admin_client):
|
|
"""
|
|
Regressão do modelo de concorrência (3b): um formulário de "Fechar
|
|
Votação" com um valor obsoleto para quem já votou pelo tablet não pode
|
|
sobrescrever esse voto — só preenche quem ainda não tem voto registrado.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
ja_votou = _presente(sessao)
|
|
ainda_nao_votou = _presente(sessao)
|
|
baker.make(VotoParlamentar, ordem=ordem, parlamentar=ja_votou, voto='Sim')
|
|
|
|
tipo_resultado = baker.make(TipoResultadoVotacao, nome='Aprovada', natureza='A')
|
|
|
|
url = _registrar_url(sessao, ordem)
|
|
payload = {
|
|
'salvar-votacao': '1',
|
|
'resultado_votacao': str(tipo_resultado.pk),
|
|
'observacao': '',
|
|
'voto_parlamentar': [
|
|
# valor obsoleto: a tela do operador ainda não sabia que este
|
|
# parlamentar já havia votado "Sim" pelo tablet
|
|
'Não Votou:{}'.format(ja_votou.pk),
|
|
'Não:{}'.format(ainda_nao_votou.pk),
|
|
],
|
|
}
|
|
|
|
response = admin_client.post(url, payload)
|
|
assert response.status_code == 302
|
|
|
|
voto_ja_votou = VotoParlamentar.objects.get(ordem=ordem, parlamentar=ja_votou)
|
|
assert voto_ja_votou.voto == 'Sim'
|
|
|
|
voto_novo = VotoParlamentar.objects.get(ordem=ordem, parlamentar=ainda_nao_votou)
|
|
assert voto_novo.voto == 'Não'
|
|
|
|
registro = RegistroVotacao.objects.get(ordem=ordem)
|
|
assert registro.numero_votos_sim == 1
|
|
assert registro.numero_votos_nao == 1
|
|
|
|
ordem.refresh_from_db()
|
|
assert ordem.votacao_aberta is False
|
|
assert ordem.registro_aberto is False
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_salvar_votacao_sem_votos_nao_trava_selects_para_nova_tentativa(admin_client):
|
|
"""
|
|
Regressão: fechar a votação sem nenhum voto real corretamente mostra um
|
|
erro, mas antes disso o laço de salvamento em lote criava um
|
|
VotoParlamentar com voto='Não Votou' para cada parlamentar cujo select
|
|
não foi tocado (o valor padrão do <select>). Esses registros persistiam
|
|
mesmo com o fechamento falhando (o bloco atomic não é revertido, já que
|
|
form_invalid retorna normalmente em vez de lançar), e nominal.html
|
|
desabilita o <select> de qualquer parlamentar com um VotoParlamentar
|
|
existente — travando o operador para sempre sem conseguir registrar
|
|
nenhum voto para essa matéria.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
parlamentar = _presente(sessao)
|
|
tipo_resultado = baker.make(TipoResultadoVotacao, nome='Aprovada', natureza='A')
|
|
|
|
url = _registrar_url(sessao, ordem)
|
|
payload = {
|
|
'salvar-votacao': '1',
|
|
'resultado_votacao': str(tipo_resultado.pk),
|
|
'observacao': '',
|
|
'voto_parlamentar': ['Não Votou:{}'.format(parlamentar.pk)],
|
|
}
|
|
|
|
response = admin_client.post(url, payload)
|
|
assert response.status_code == 302
|
|
|
|
assert not VotoParlamentar.objects.filter(
|
|
ordem=ordem, parlamentar=parlamentar).exists()
|
|
|
|
ordem.refresh_from_db()
|
|
assert ordem.votacao_aberta is True
|
|
|
|
# O operador consegue tentar de novo, agora com um voto real.
|
|
response2 = admin_client.post(url, {
|
|
'salvar-votacao': '1',
|
|
'resultado_votacao': str(tipo_resultado.pk),
|
|
'observacao': '',
|
|
'voto_parlamentar': ['Sim:{}'.format(parlamentar.pk)],
|
|
})
|
|
assert response2.status_code == 302
|
|
ordem.refresh_from_db()
|
|
assert ordem.votacao_aberta is False
|
|
assert VotoParlamentar.objects.get(ordem=ordem, parlamentar=parlamentar).voto == 'Sim'
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_status_da_votacao_reflete_troca_de_voto_do_parlamentar(admin_client):
|
|
"""
|
|
Regressão: a tela de registro (nominal.html) não atualizava a linha de
|
|
um parlamentar que trocou o voto durante a janela de votação — o poll
|
|
antigo só marcava "já votou" uma vez e nunca revisitava o valor. O poll
|
|
(?status=1) precisa sempre devolver o voto atual, não só se existe.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
parlamentar = _presente(sessao)
|
|
voto = baker.make(VotoParlamentar, ordem=ordem, parlamentar=parlamentar,
|
|
voto='Sim')
|
|
|
|
url = _registrar_url(sessao, ordem) + '?status=1'
|
|
response = admin_client.get(url)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['votos'] == {str(parlamentar.pk): 'Sim'}
|
|
assert data['votacao_aberta'] is True
|
|
assert data['registro_aberto'] is False
|
|
assert data['ja_registrada'] is False
|
|
|
|
voto.voto = 'Não'
|
|
voto.save()
|
|
|
|
response2 = admin_client.get(url)
|
|
assert response2.json()['votos'] == {str(parlamentar.pk): 'Não'}
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_status_da_votacao_nao_depende_de_mostrar_voto(admin_client):
|
|
"""
|
|
O poll da tela de registro é só para a Mesa, não para o público — ao
|
|
contrário de sapl.painel:dados_painel, ele não pode mascarar o valor
|
|
real do voto por trás de "Voto Informado" mesmo quando a Casa configura
|
|
mostrar_voto=False (essa config controla o telão público, não a tela de
|
|
registro da própria Mesa). Como o endpoint nem consulta essa
|
|
configuração, isso é garantido por construção — este teste só
|
|
documenta a expectativa.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
parlamentar = _presente(sessao)
|
|
baker.make(VotoParlamentar, ordem=ordem, parlamentar=parlamentar,
|
|
voto='Abstenção')
|
|
|
|
url = _registrar_url(sessao, ordem) + '?status=1'
|
|
data = admin_client.get(url).json()
|
|
assert data['votos'][str(parlamentar.pk)] == 'Abstenção'
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_status_da_votacao_nao_redireciona_apos_encerrar_votacao(
|
|
admin_client):
|
|
"""
|
|
_get_materia_votacao (usado pelo GET normal) redireciona com uma
|
|
mensagem quando a matéria já foi votada — comportamento certo para
|
|
navegação, errado para um poll em background. O branch ?status=1 não
|
|
pode herdar esse redirect.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
parlamentar = _presente(sessao)
|
|
tipo_resultado = baker.make(TipoResultadoVotacao, nome='Aprovada',
|
|
natureza='A')
|
|
|
|
url = _registrar_url(sessao, ordem)
|
|
admin_client.post(url, {
|
|
'salvar-votacao': '1',
|
|
'resultado_votacao': str(tipo_resultado.pk),
|
|
'observacao': '',
|
|
'voto_parlamentar': ['Sim:{}'.format(parlamentar.pk)],
|
|
})
|
|
|
|
response = admin_client.get(url + '?status=1')
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data['ja_registrada'] is True
|
|
assert data['votacao_aberta'] is False
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_abrir_votacao_ja_aberta_e_idempotente(admin_client):
|
|
"""
|
|
Regressão: clicar em "Abrir Votação" para uma matéria que já está aberta
|
|
(ex.: duplo clique, ou a lista ainda não recarregou para trocar o botão
|
|
por "Registrar Votação") caía em verifica_votacoes_abertas(), que trata
|
|
a própria matéria como uma "outra" votação conflitante — mostra a
|
|
mensagem "existem votações abertas... foram fechadas" e fecha a matéria
|
|
(para reabri-la em seguida, já que o código sempre prossegue). O
|
|
resultado final até ficava certo (votacao_aberta=True), mas a mensagem
|
|
era enganosa. Reabrir a própria matéria já aberta precisa ser
|
|
silenciosamente idempotente.
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
sessao.iniciada = True
|
|
sessao.finalizada = False
|
|
sessao.save()
|
|
_presente(sessao)
|
|
|
|
url = reverse('sapl.sessao:abrir_votacao', kwargs={'pk': ordem.pk, 'spk': sessao.pk})
|
|
url += '?tipo_materia=ordem'
|
|
|
|
response = admin_client.get(url, follow=True)
|
|
|
|
assert response.status_code == 200
|
|
ordem.refresh_from_db()
|
|
assert ordem.votacao_aberta is True
|
|
|
|
mensagens = [str(m) for m in response.context['messages']]
|
|
assert not any('foram fechadas' in m for m in mensagens)
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_unique_constraint_impede_voto_duplicado(admin_client):
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
parlamentar = _presente(sessao)
|
|
baker.make(VotoParlamentar, ordem=ordem, parlamentar=parlamentar, voto='Sim')
|
|
|
|
with pytest.raises(IntegrityError):
|
|
VotoParlamentar.objects.create(
|
|
ordem=ordem, parlamentar=parlamentar, voto='Não')
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_unique_constraint_impede_duas_ordens_abertas():
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
outra = baker.make(OrdemDia, sessao_plenaria=sessao, materia=ordem.materia,
|
|
tipo_votacao=NOMINAL, votacao_aberta=False)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
outra.votacao_aberta = True
|
|
outra.save()
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_migracao_0070_remove_votos_duplicados_antes_da_constraint():
|
|
import importlib
|
|
|
|
from django.apps import apps as real_apps
|
|
from django.db import connection
|
|
|
|
migracao = importlib.import_module(
|
|
'sapl.sessao.migrations.0070_votoparlamentar_unique_constraint')
|
|
|
|
with connection.cursor() as cursor:
|
|
cursor.execute(
|
|
'DROP INDEX sessao_votoparlamentar_unique_parlamentar_ordem')
|
|
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
parlamentar = _presente(sessao)
|
|
antigo = baker.make(VotoParlamentar, ordem=ordem,
|
|
parlamentar=parlamentar, voto='Não')
|
|
recente = baker.make(VotoParlamentar, ordem=ordem,
|
|
parlamentar=parlamentar, voto='Sim')
|
|
|
|
migracao.remove_votos_duplicados(real_apps, None)
|
|
|
|
votos = VotoParlamentar.objects.filter(
|
|
ordem=ordem, parlamentar=parlamentar)
|
|
assert list(votos.values_list('id', flat=True)) == [recente.id]
|
|
assert not votos.filter(id=antigo.id).exists()
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_migracao_0071_fecha_duplicatas_antes_da_constraint():
|
|
"""
|
|
A função de dados da migration 0071 precisa fechar duplicatas
|
|
pré-existentes antes do AddConstraint — senão a migration falharia ao
|
|
ser aplicada num banco com dado antigo (de antes desta invariante
|
|
existir). Testa a função isoladamente: como o teste roda dentro de uma
|
|
transação que é desfeita no final, é seguro derrubar o índice aqui
|
|
(DDL é transacional no Postgres).
|
|
"""
|
|
import importlib
|
|
|
|
from django.apps import apps as real_apps
|
|
from django.db import connection
|
|
|
|
migracao = importlib.import_module(
|
|
'sapl.sessao.migrations.0071_votacao_aberta_unique_constraint')
|
|
|
|
with connection.cursor() as cursor:
|
|
cursor.execute('DROP INDEX sessao_ordemdia_unique_votacao_aberta')
|
|
|
|
sessao, mais_antiga = _ordem_nominal_aberta()
|
|
mais_recente = baker.make(OrdemDia, sessao_plenaria=sessao,
|
|
materia=mais_antiga.materia, tipo_votacao=NOMINAL,
|
|
votacao_aberta=True)
|
|
assert mais_recente.pk > mais_antiga.pk
|
|
|
|
migracao.fecha_matérias_abertas_duplicadas(real_apps, None)
|
|
|
|
mais_antiga.refresh_from_db()
|
|
mais_recente.refresh_from_db()
|
|
assert mais_antiga.votacao_aberta is False
|
|
assert mais_recente.votacao_aberta is True
|
|
|
|
|
|
@pytest.mark.django_db(transaction=False)
|
|
def test_api_nao_permite_abrir_votacao_via_patch(admin_client):
|
|
"""
|
|
Regressão do bypass encontrado na auditoria: a API auto-gerada
|
|
(drfautoapi) não pode mais aceitar votacao_aberta/registro_aberto —
|
|
senão qualquer "Operador de Sessão Plenária" conseguiria abrir uma
|
|
matéria via PATCH direto, pulando verifica_votacoes_abertas() e o
|
|
lock em abrir_votacao().
|
|
"""
|
|
sessao, ordem = _ordem_nominal_aberta()
|
|
ordem.votacao_aberta = False
|
|
ordem.save()
|
|
|
|
url = '/api/sessao/ordemdia/{}/'.format(ordem.pk)
|
|
response = admin_client.patch(
|
|
url, data={'votacao_aberta': True}, content_type='application/json')
|
|
|
|
assert response.status_code in (200, 202)
|
|
ordem.refresh_from_db()
|
|
assert ordem.votacao_aberta is False
|
|
|