"""Tests para pg_query. Requieren un PostgreSQL real. Si la variable de entorno PG_TEST_DSN no esta definida, todos los tests se saltan con skip elegante (no fallan). Cada test crea y limpia su propia tabla temporal con un nombre aleatorio para no depender de un schema concreto ni interferir entre ejecuciones. PG_TEST_DSN="postgresql://user:pass@localhost:5433/trends" \ python/.venv/bin/python3 -m pytest python/functions/infra/pg_query_test.py """ import base64 import os import sys import uuid from datetime import date import pytest sys.path.insert( 0, os.path.join(os.path.dirname(__file__), "..") ) # python/functions -> permite `from infra...` from infra.pg_query import _to_serializable, pg_query # noqa: E402 PG_TEST_DSN = os.environ.get("PG_TEST_DSN") requires_pg = pytest.mark.skipif( not PG_TEST_DSN, reason="PG_TEST_DSN no definido: se omiten los tests de Postgres" ) @pytest.fixture def temp_table(): """Crea una tabla temporal con datos y la elimina al terminar.""" import psycopg2 name = "pg_query_t_" + uuid.uuid4().hex[:12] conn = psycopg2.connect(PG_TEST_DSN) try: with conn.cursor() as cur: cur.execute( f"CREATE TABLE {name} (id INTEGER, region TEXT, total NUMERIC(10,2))" ) cur.execute( f"INSERT INTO {name} VALUES (1,'norte',120.50),(2,'sur',80.00)," f"(3,'norte',45.25)" ) conn.commit() finally: conn.close() yield name conn = psycopg2.connect(PG_TEST_DSN) try: with conn.cursor() as cur: cur.execute(f"DROP TABLE IF EXISTS {name}") conn.commit() finally: conn.close() def test_skip_sin_pg_test_dsn(): """skip sin PG_TEST_DSN: el resto de tests no corre sin Postgres.""" if not PG_TEST_DSN: pytest.skip("PG_TEST_DSN no definido") # Si hay DSN, el placeholder se cumple trivialmente. assert PG_TEST_DSN def test_normaliza_tipos_no_serializables(): """normaliza tipos no serializables: _to_serializable es pura, sin DB.""" assert _to_serializable(date(2026, 6, 16)) == "2026-06-16" assert _to_serializable(uuid.UUID(int=0)) == str(uuid.UUID(int=0)) assert _to_serializable(b"\x00\x01") == base64.b64encode(b"\x00\x01").decode("ascii") import decimal assert _to_serializable(decimal.Decimal("1.50")) == 1.5 assert _to_serializable(None) is None assert _to_serializable("x") == "x" @requires_pg def test_select_con_parametros_posicionales(temp_table): """select con parametros posicionales: filtra por %s, agrega y serializa.""" res = pg_query( PG_TEST_DSN, f"SELECT region, SUM(total) AS total FROM {temp_table} " f"WHERE region = %s GROUP BY region", params=["norte"], ) assert res["status"] == "ok" assert res["columns"] == ["region", "total"] assert res["row_count"] == 1 assert res["rows"][0]["region"] == "norte" # NUMERIC se normaliza a float. assert abs(res["rows"][0]["total"] - 165.75) < 1e-9 assert res["truncated"] is False @requires_pg def test_trunca_a_max_rows(temp_table): """trunca a max_rows: pide menos filas de las que hay y marca truncated.""" res = pg_query(PG_TEST_DSN, f"SELECT id FROM {temp_table} ORDER BY id", max_rows=2) assert res["status"] == "ok" assert res["row_count"] == 2 assert res["truncated"] is True @requires_pg def test_dsn_invalido_devuelve_status_error(): """dsn invalido devuelve status error sin lanzar.""" res = pg_query( "postgresql://nouser:nopass@127.0.0.1:1/nodb", "SELECT 1", ) assert res["status"] == "error" assert "error" in res