"""Tests para pg_upsert. Requieren un PostgreSQL real. Si PG_TEST_DSN no esta definida, los tests que tocan la DB se saltan con skip elegante. Cada test crea y limpia su propia tabla con un nombre aleatorio. PG_TEST_DSN="postgresql://user:pass@localhost:5433/trends" \ python/.venv/bin/python3 -m pytest python/functions/infra/pg_upsert_test.py """ import os import sys import uuid import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from infra.pg_upsert import pg_upsert # 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 leads(email PRIMARY KEY, name TEXT, score INT) y la elimina al final.""" import psycopg2 name = "pg_upsert_t_" + uuid.uuid4().hex[:12] conn = psycopg2.connect(PG_TEST_DSN) try: with conn.cursor() as cur: cur.execute( f"CREATE TABLE {name} " f"(email TEXT PRIMARY KEY, name TEXT, score INTEGER)" ) 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 _read(table, email): import psycopg2 conn = psycopg2.connect(PG_TEST_DSN) try: with conn.cursor() as cur: cur.execute( f"SELECT name, score FROM {table} WHERE email = %s", (email,) ) return cur.fetchone() finally: conn.close() def test_skip_sin_pg_test_dsn(): """skip sin PG_TEST_DSN.""" if not PG_TEST_DSN: pytest.skip("PG_TEST_DSN no definido") assert PG_TEST_DSN def test_identificador_invalido_devuelve_status_error(): """identificador invalido devuelve status error sin tocar DB.""" res = pg_upsert( "postgresql://x/y", "tabla mala; DROP TABLE foo", [{"email": "a@x.com"}], key_cols=["email"], ) assert res["status"] == "error" @requires_pg def test_inserta_filas_nuevas_cuenta_inserted(temp_table): """inserta filas nuevas cuenta inserted.""" res = pg_upsert( PG_TEST_DSN, temp_table, [ {"email": "ana@x.com", "name": "Ana", "score": 0}, {"email": "bob@x.com", "name": "Bob", "score": 5}, ], key_cols=["email"], ) assert res["status"] == "ok" assert res["inserted"] == 2 assert res["updated"] == 0 @requires_pg def test_conflicto_actualiza_y_cuenta_updated(temp_table): """conflicto actualiza columnas y cuenta updated.""" pg_upsert( PG_TEST_DSN, temp_table, [{"email": "ana@x.com", "name": "Ana", "score": 0}], key_cols=["email"], ) res = pg_upsert( PG_TEST_DSN, temp_table, [{"email": "ana@x.com", "name": "Ana Lopez", "score": 9}], key_cols=["email"], ) assert res["status"] == "ok" assert res["inserted"] == 0 assert res["updated"] == 1 assert _read(temp_table, "ana@x.com") == ("Ana Lopez", 9) @requires_pg def test_ownership_selectivo_no_pisa_columna_excluida(temp_table): """ownership selectivo no pisa columna excluida.""" pg_upsert( PG_TEST_DSN, temp_table, [{"email": "ana@x.com", "name": "Ana", "score": 0}], key_cols=["email"], ) # La DB es duena de score (otro proceso lo subio a 87). import psycopg2 conn = psycopg2.connect(PG_TEST_DSN) with conn.cursor() as cur: cur.execute(f"UPDATE {temp_table} SET score = 87 WHERE email = 'ana@x.com'") conn.commit() conn.close() # El feed trae score=0 pero solo autorizamos actualizar name. res = pg_upsert( PG_TEST_DSN, temp_table, [{"email": "ana@x.com", "name": "Ana Lopez", "score": 0}], key_cols=["email"], update_cols=["name"], ) assert res["status"] == "ok" assert res["updated"] == 1 assert _read(temp_table, "ana@x.com") == ("Ana Lopez", 87) @requires_pg def test_do_nothing_no_actualiza(temp_table): """do nothing no actualiza: update_cols=[] inserta solo nuevas.""" pg_upsert( PG_TEST_DSN, temp_table, [{"email": "ana@x.com", "name": "Ana", "score": 1}], key_cols=["email"], ) res = pg_upsert( PG_TEST_DSN, temp_table, [ {"email": "ana@x.com", "name": "PISADO", "score": 99}, # conflicto {"email": "new@x.com", "name": "Nuevo", "score": 2}, # nuevo ], key_cols=["email"], update_cols=[], ) assert res["status"] == "ok" # La fila en conflicto no se devuelve por RETURNING (DO NOTHING). assert res["inserted"] == 1 assert res["updated"] == 0 # La existente NO se pisa. assert _read(temp_table, "ana@x.com") == ("Ana", 1)