File size: 6,931 Bytes
5340e92 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """Rotas HTTP da API e a integração AIDA Image → AIDA Forensics.
O pipeline é um dublê (ver `conftest.py`): o que está sob teste aqui é o
contrato da API — códigos de status, forma da resposta e o caminho que leva do
resultado da análise ao dossiê forense sem recalcular nada.
"""
from __future__ import annotations
import io
import pytest
from PIL import Image
def _imagem_em_memoria(formato: str = "JPEG", nome: str = "foto.jpg"):
buffer = io.BytesIO()
Image.new("RGB", (48, 48), (40, 120, 110)).save(buffer, format=formato)
buffer.seek(0)
return (buffer, nome)
# --------------------------------------------------------------------------- #
# Rotas de diagnóstico
# --------------------------------------------------------------------------- #
def test_raiz_lista_os_quatro_estados(cliente_api):
corpo = cliente_api.get("/").get_json()
assert corpo["status"] == "online"
assert set(corpo["estados_possiveis"]) == {"REAL", "REAL_EDITADA", "IA/MANIPULADA", "INCONCLUSIVO"}
assert corpo["forense"] == "/forense/<id_analise>"
def test_saude_reporta_o_cache(cliente_api):
"""Sem cache não há Forensics; a queda precisa aparecer no diagnóstico."""
corpo = cliente_api.get("/saude").get_json()
assert "cache_analises" in corpo
assert corpo["cache_analises"]["entradas"] == 0
assert corpo["cache_analises"]["ttl_horas"] > 0
def test_contrato_declara_o_endpoint_forense(cliente_api):
corpo = cliente_api.get("/contrato").get_json()
assert "forense_url" in corpo["resposta_200"]
assert "GET /forense/<id>" in corpo["endpoints_relacionados"]
# --------------------------------------------------------------------------- #
# Envio e análise
# --------------------------------------------------------------------------- #
def test_analisar_sem_imagem(cliente_api):
resposta = cliente_api.post("/analisar", data={})
assert resposta.status_code == 400
assert "erro" in resposta.get_json()
def test_analisar_formato_nao_aceito(cliente_api):
resposta = cliente_api.post(
"/analisar",
data={"imagem": (io.BytesIO(b"nao sou imagem"), "documento.txt")},
content_type="multipart/form-data",
)
assert resposta.status_code == 400
assert "formatos_aceitos" in resposta.get_json()
def test_analisar_arquivo_invalido_com_extensao_valida(cliente_api):
"""Extensão certa não é garantia de conteúdo: o arquivo é verificado."""
resposta = cliente_api.post(
"/analisar",
data={"imagem": (io.BytesIO(b"isto nao e um jpeg"), "mentira.jpg")},
content_type="multipart/form-data",
)
assert resposta.status_code == 400
def test_analisar_devolve_resultado_e_ponte_forense(cliente_api):
resposta = cliente_api.post(
"/analisar",
data={"imagem": _imagem_em_memoria()},
content_type="multipart/form-data",
)
assert resposta.status_code == 200
corpo = resposta.get_json()
assert corpo["sucesso"] is True
assert corpo["resultado"] == "INCONCLUSIVO"
assert corpo["forense_url"] == f"/forense/{corpo['id_analise']}"
# O vetor de características não pode viajar na resposta HTTP.
assert "principais_metricas" not in corpo
def test_analisar_preserva_o_nome_enviado(cliente_api):
"""O arquivo temporário do servidor tem o id por nome; o original se perderia."""
resposta = cliente_api.post(
"/analisar",
data={"imagem": _imagem_em_memoria(nome="paisagem-praia.jpg")},
content_type="multipart/form-data",
)
assert resposta.get_json()["nome_imagem_original"] == "paisagem-praia.jpg"
def test_nome_enviado_e_saneado(cliente_api):
"""O nome vem do cliente e vai para a tela: separadores de caminho saem."""
resposta = cliente_api.post(
"/analisar",
data={"imagem": _imagem_em_memoria(nome="../../etc/passwd.jpg")},
content_type="multipart/form-data",
)
nome = resposta.get_json()["nome_imagem_original"]
assert "/" not in nome and "\\" not in nome
# --------------------------------------------------------------------------- #
# Integração AIDA Image → AIDA Forensics
# --------------------------------------------------------------------------- #
def test_forense_reabre_a_analise_sem_recalcular(cliente_api):
analise = cliente_api.post(
"/analisar",
data={"imagem": _imagem_em_memoria()},
content_type="multipart/form-data",
).get_json()
chamadas_antes = len(cliente_api.analises_feitas)
dossie = cliente_api.get(analise["forense_url"]).get_json()
# A garantia central desta funcionalidade: abrir o Forensics não dispara
# uma segunda execução do pipeline.
assert len(cliente_api.analises_feitas) == chamadas_antes
assert dossie["id_analise"] == analise["id_analise"]
assert dossie["origem"] == "cache"
assert dossie["resultado"]["resultado"] == analise["resultado"]
assert dossie["resultado"]["probabilidade_ia_exibicao"] == pytest.approx(
analise["probabilidade_ia_exibicao"]
)
assert dossie["metodos"]
assert dossie["evidencias"]["mapas"]
def test_mapas_do_dossie_sao_servidos(cliente_api):
analise = cliente_api.post(
"/analisar",
data={"imagem": _imagem_em_memoria()},
content_type="multipart/form-data",
).get_json()
dossie = cliente_api.get(analise["forense_url"]).get_json()
for mapa in dossie["evidencias"]["mapas"]:
assert cliente_api.get(mapa["url"]).status_code == 200
def test_forense_de_analise_inexistente(cliente_api):
resposta = cliente_api.get("/forense/nao-existe")
assert resposta.status_code == 404
corpo = resposta.get_json()
# A mensagem precisa dizer o que fazer, não só que falhou.
assert corpo["acao"]
assert corpo["motivo"]
def test_forense_recusa_identificador_com_travessia(cliente_api):
"""`..%2f..` não pode alcançar arquivo fora do diretório de cache."""
resposta = cliente_api.get("/forense/..%2f..%2fsegredo")
assert resposta.status_code in (404, 400)
def test_delete_forense_remove_do_cache(cliente_api):
analise = cliente_api.post(
"/analisar",
data={"imagem": _imagem_em_memoria()},
content_type="multipart/form-data",
).get_json()
apagar = cliente_api.delete(analise["forense_url"]).get_json()
assert apagar["removida"] is True
assert cliente_api.get(analise["forense_url"]).status_code == 404
# Segunda remoção não é erro; é apenas nada a remover.
assert cliente_api.delete(analise["forense_url"]).get_json()["removida"] is False
def test_evidencia_com_identificador_invalido(cliente_api):
assert cliente_api.get("/evidencia/..%2f..%2fetc/passwd").status_code in (400, 404)
def test_analise_inexistente_no_historico(cliente_api):
assert cliente_api.get("/analise/nao-existe-123").status_code == 404
|