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