Compare commits
No commits in common. "c058bc2cf1a59299530c1d6bdd72c1d3b6f6a9e8" and "9cf4547925dc61929f6632e794ab99ec2e39b987" have entirely different histories.
c058bc2cf1
...
9cf4547925
@ -1,202 +0,0 @@
|
|||||||
"""Red de seguridad: que todas las vistas rendericen y que el contexto no pierda claves.
|
|
||||||
|
|
||||||
Dos agujeros que ni `manage.py check` ni el resto de la suite cubren:
|
|
||||||
|
|
||||||
1. Un `{% url %}` mal escrito o una plantilla que falta solo revienta al pedir
|
|
||||||
la página. Es lo que pasó con `settings/index.html`, que estuvo roto sin que
|
|
||||||
nada avisara.
|
|
||||||
2. Un refactor que se lleva una clave del contexto por delante. Ya pasó dos
|
|
||||||
veces: `by_month` renombrado a `chart_data`, y `last_expenses` sustituido por
|
|
||||||
`movements`. Las claves que usan muchos tests están cubiertas de rebote; las
|
|
||||||
que se usan en un solo sitio se pierden en silencio.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from expenses import urls as expenses_urls
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# 1. Smoke test de renderizado
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Rutas que a propósito NO devuelven 200. Van una por línea con su motivo: la
|
|
||||||
# alternativa (aceptar cualquier 2xx/3xx) haría pasar el test aunque la vista
|
|
||||||
# estuviera rota, que es justo lo contrario de lo que se busca aquí.
|
|
||||||
NOT_RENDERABLE = {
|
|
||||||
# Endpoint de descarga, no página: valida ExportForm(request.GET) y sin
|
|
||||||
# `kind` ni `fmt` responde 400 por diseño. Su cobertura está en test_export.
|
|
||||||
"export_download",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _argument_free_route_names():
|
|
||||||
"""Rutas del URLconf que se pueden pedir sin argumentos.
|
|
||||||
|
|
||||||
Se saca del URLconf y no de una lista escrita a mano: así una vista nueva
|
|
||||||
entra en el smoke test el día que se añade, sin que nadie se acuerde. Una
|
|
||||||
lista a mano se queda desfasada y el test deja de servir en silencio.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
pattern.name
|
|
||||||
for pattern in expenses_urls.urlpatterns
|
|
||||||
if pattern.pattern.regex.groups == 0
|
|
||||||
and pattern.name
|
|
||||||
and pattern.name not in NOT_RENDERABLE
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
ROUTE_NAMES = _argument_free_route_names()
|
|
||||||
|
|
||||||
|
|
||||||
def test_the_route_list_is_not_empty():
|
|
||||||
# Si el recorrido del URLconf se rompiera, el parametrize saldría vacío y
|
|
||||||
# los tests de abajo "pasarían" sin pedir una sola página.
|
|
||||||
assert len(ROUTE_NAMES) > 15
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("route_name", ROUTE_NAMES)
|
|
||||||
def test_every_view_renders(auth_client, route_name):
|
|
||||||
# Una ruta por caso para que el fallo diga qué vista se rompió.
|
|
||||||
response = auth_client.get(reverse(route_name))
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("route_name", sorted(NOT_RENDERABLE))
|
|
||||||
def test_the_excluded_routes_are_still_excluded_for_a_reason(auth_client, route_name):
|
|
||||||
# Si una excluida empieza a devolver 200, la exclusión sobra y hay que
|
|
||||||
# quitarla: así la lista no se queda con motivos caducados dentro.
|
|
||||||
response = auth_client.get(reverse(route_name))
|
|
||||||
|
|
||||||
assert response.status_code != 200
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# 2. Claves del contexto
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
DASHBOARD_CONTEXT_KEYS = {
|
|
||||||
# Navegación y fecha de referencia
|
|
||||||
"active_menu",
|
|
||||||
"today",
|
|
||||||
# KPIs
|
|
||||||
"kpi_total",
|
|
||||||
"kpi_count",
|
|
||||||
"kpi_categories",
|
|
||||||
"kpi_balance",
|
|
||||||
# Comparativa con el periodo anterior
|
|
||||||
"compare_enabled",
|
|
||||||
"compare_suppressed",
|
|
||||||
"kpi_previous_total",
|
|
||||||
"kpi_difference",
|
|
||||||
"kpi_difference_abs",
|
|
||||||
"kpi_percentage",
|
|
||||||
"kpi_trend",
|
|
||||||
"category_comparison",
|
|
||||||
# Gráficos: eje compartido, serie de gastos, serie de ingresos y desglose
|
|
||||||
"chart_labels",
|
|
||||||
"chart_data",
|
|
||||||
"chart_income_data",
|
|
||||||
"chart_type",
|
|
||||||
"by_category",
|
|
||||||
"by_category_chart",
|
|
||||||
"accounts_charts",
|
|
||||||
# Filtros de periodo, cuenta y rango libre
|
|
||||||
"period",
|
|
||||||
"year_list",
|
|
||||||
"months",
|
|
||||||
"selected_year",
|
|
||||||
"selected_month",
|
|
||||||
"date_from",
|
|
||||||
"date_to",
|
|
||||||
"range_active",
|
|
||||||
"accounts",
|
|
||||||
"selected_account",
|
|
||||||
"selected_account_obj",
|
|
||||||
# Objetivos y listado reciente
|
|
||||||
"goals",
|
|
||||||
"recent_expenses",
|
|
||||||
# Medias y proyección
|
|
||||||
"daily_average",
|
|
||||||
"projected_end_of_month",
|
|
||||||
}
|
|
||||||
|
|
||||||
HOME_CONTEXT_KEYS = {
|
|
||||||
"active_menu",
|
|
||||||
# KPIs del mes y su comparación con el anterior
|
|
||||||
"kpi_total",
|
|
||||||
"kpi_count",
|
|
||||||
"kpi_categories",
|
|
||||||
"prev_total",
|
|
||||||
"diff_amount",
|
|
||||||
"diff_pct",
|
|
||||||
# Saldos y alertas
|
|
||||||
"total_balance",
|
|
||||||
"account_balances",
|
|
||||||
"negative_accounts",
|
|
||||||
"has_alerts",
|
|
||||||
# Objetivos
|
|
||||||
"goals",
|
|
||||||
"exceeded_goals",
|
|
||||||
# Movimientos recientes y la mini gráfica
|
|
||||||
"movements",
|
|
||||||
"mini_chart_labels",
|
|
||||||
"mini_chart_data",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _assert_context_has(response, expected_keys):
|
|
||||||
"""Inclusión, nunca igualdad.
|
|
||||||
|
|
||||||
Lo que hay que detectar es que falte una clave. Que aparezca una nueva es lo
|
|
||||||
normal al añadir una funcionalidad, así que endurecer esto a igualdad exacta
|
|
||||||
daría un test que falla cada vez que alguien amplía la vista, y acabaría
|
|
||||||
borrado por pesado. No lo cambies a ==.
|
|
||||||
"""
|
|
||||||
missing = sorted(key for key in expected_keys if key not in response.context)
|
|
||||||
|
|
||||||
assert not missing, "faltan claves en el contexto: %s" % ", ".join(missing)
|
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_context_keeps_all_its_keys(auth_client):
|
|
||||||
response = _dashboard(auth_client)
|
|
||||||
|
|
||||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_context_keeps_its_keys_with_comparison_on(auth_client):
|
|
||||||
# La comparativa rellena claves que en el camino normal se quedan a cero.
|
|
||||||
response = _dashboard(auth_client, compare="1")
|
|
||||||
|
|
||||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_context_keeps_its_keys_with_a_free_range(auth_client):
|
|
||||||
# El rango libre entra por otra rama del eje, y es la que trae
|
|
||||||
# compara_suppressed y deja projected_end_of_month a cero.
|
|
||||||
response = _dashboard(auth_client, date_from="2024-03-01", date_to="2024-03-30")
|
|
||||||
|
|
||||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_context_keeps_its_keys_with_a_preset(auth_client):
|
|
||||||
response = _dashboard(auth_client, period="this_month")
|
|
||||||
|
|
||||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
|
||||||
|
|
||||||
|
|
||||||
def test_home_context_keeps_all_its_keys(auth_client):
|
|
||||||
response = auth_client.get(reverse("home"))
|
|
||||||
|
|
||||||
_assert_context_has(response, HOME_CONTEXT_KEYS)
|
|
||||||
|
|
||||||
|
|
||||||
def _dashboard(client, **params):
|
|
||||||
response = client.get(reverse("dashboard"), params)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
return response
|
|
||||||
Loading…
Reference in New Issue
Block a user