feat: protecao CSRF (double-submit cookie) e paginacao da lista de clientes
CSRF: novo app/csrf.py + middleware em app/main.py garantem um cookie csrf_token por navegador; todo formulario que muda estado (criar/editar/ excluir cliente, adicionar/editar/excluir contato) embute esse valor como campo oculto, e Depends(exigir_csrf) confere no POST que o valor enviado bate com o cookie antes de deixar a rota executar. Verificado ponta a ponta com o app real rodando (curl): token errado -> 403, token correto -> 303. Paginacao: db_clientes.listar_clientes ganha offset, nova contar_clientes(); GET /clientes aceita ?pagina=N (100 clientes por pagina), template mostra Anterior/Proxima e o total. 53/53 testes passando (8 novos).
This commit is contained in:
+37
-4
@@ -1,8 +1,11 @@
|
||||
import base64
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
_AUTH_HEADERS = {"Authorization": f"Basic {base64.b64encode(b'admin:teste123').decode()}"}
|
||||
|
||||
|
||||
def test_raiz_sem_login_pede_credenciais():
|
||||
@@ -19,9 +22,39 @@ def test_docs_desabilitado_retorna_404():
|
||||
def test_raiz_redireciona_para_clientes_com_login(monkeypatch):
|
||||
from app.routers import clientes as router_clientes
|
||||
|
||||
monkeypatch.setattr(router_clientes.db_clientes, "listar_clientes", lambda busca=None, limite=100: [])
|
||||
import base64
|
||||
token = base64.b64encode(b"admin:teste123").decode()
|
||||
resposta = client.get("/", headers={"Authorization": f"Basic {token}"}, follow_redirects=False)
|
||||
monkeypatch.setattr(router_clientes.db_clientes, "listar_clientes", lambda busca=None, limite=100, offset=0: [])
|
||||
monkeypatch.setattr(router_clientes.db_clientes, "contar_clientes", lambda busca=None: 0)
|
||||
resposta = client.get("/", headers=_AUTH_HEADERS, follow_redirects=False)
|
||||
assert resposta.status_code == 307
|
||||
assert resposta.headers["location"] == "/clientes"
|
||||
|
||||
|
||||
def test_csrf_middleware_define_cookie_em_qualquer_resposta():
|
||||
client.cookies.clear()
|
||||
resposta = client.get("/docs")
|
||||
assert resposta.status_code == 404
|
||||
assert "csrf_token" in resposta.cookies
|
||||
|
||||
|
||||
def test_csrf_bloqueia_post_com_token_que_nao_bate_com_cookie():
|
||||
client.cookies.clear()
|
||||
resposta = client.post(
|
||||
"/clientes/1/excluir", headers=_AUTH_HEADERS, data={"csrf_token": "token-forjado-sem-cookie"}
|
||||
)
|
||||
assert resposta.status_code == 403
|
||||
|
||||
|
||||
def test_csrf_libera_post_quando_token_bate_com_cookie(monkeypatch):
|
||||
from app.routers import clientes as router_clientes
|
||||
|
||||
monkeypatch.setattr(router_clientes.db_clientes, "excluir_cliente", lambda cliente_id: None)
|
||||
|
||||
client.cookies.clear()
|
||||
client.get("/docs") # só para o middleware definir o cookie csrf_token no client de teste
|
||||
token_cookie = client.cookies.get("csrf_token")
|
||||
assert token_cookie
|
||||
|
||||
resposta = client.post(
|
||||
"/clientes/1/excluir", headers=_AUTH_HEADERS, data={"csrf_token": token_cookie}, follow_redirects=False
|
||||
)
|
||||
assert resposta.status_code == 303
|
||||
|
||||
Reference in New Issue
Block a user