From 382655a14d9482498c7f0b8de9126a1bf29b2271 Mon Sep 17 00:00:00 2001 From: Ochoa-Stack Date: Sun, 26 Jul 2026 22:14:08 -0600 Subject: [PATCH 1/7] ci(tests): add pytest coverage workflow with Codecov upload and ephemeral Postgres 18 service --- .github/workflows/tests.yml | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..554d36d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,72 @@ +name: Tests + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:18 + env: + POSTGRES_USER: usuario + POSTGRES_PASSWORD: password + POSTGRES_DB: skillstat_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + working-directory: backend + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run database migrations + working-directory: backend + run: flask db upgrade + env: + FLASK_ENV: "testing" + SECRET_KEY: "test-secret-key" + DATABASE_URL: "postgresql://usuario:password@localhost:5432/skillstat_test" + JWT_SECRET_KEY: "test-jwt-secret" + GOOGLE_CLIENT_ID: "test-google-client-id" + FRONTEND_BASE_URL: "http://localhost:5500/frontend" + PIPELINE_TRIGGER_SECRET: "test-pipeline-secret" + + - name: Run tests with coverage + working-directory: backend + run: pytest --cov=app --cov-report=xml + env: + FLASK_ENV: "testing" + SECRET_KEY: "test-secret-key" + DATABASE_URL: "postgresql://usuario:password@localhost:5432/skillstat_test" + JWT_SECRET_KEY: "test-jwt-secret" + GOOGLE_CLIENT_ID: "test-google-client-id" + FRONTEND_BASE_URL: "http://localhost:5500/frontend" + PIPELINE_TRIGGER_SECRET: "test-pipeline-secret" + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: backend/coverage.xml + fail_ci_if_error: false From c6b1767bfd5a5b4dd61b944b72018b609123d41d Mon Sep 17 00:00:00 2001 From: Ochoa-Stack Date: Sun, 26 Jul 2026 22:26:52 -0600 Subject: [PATCH 2/7] fix(deps): remove duplicate pytest-cov from requirements.txt --- backend/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 7b13945..5b500ef 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -46,4 +46,3 @@ Flask-Limiter>=3.8,<4.0 # Testing pytest>=8.0,<9.0 pytest-flask>=1.3,<2.0 -pytest-cov>=5.0,<6.0 From bbdd51142ec661877cf55027e8d0df7ecd6dfec2 Mon Sep 17 00:00:00 2001 From: Ochoa-Stack Date: Sun, 26 Jul 2026 22:38:39 -0600 Subject: [PATCH 3/7] ci(tests): add missing required environment variables for flask db upgrade --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 554d36d..282f7b0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,6 +51,7 @@ jobs: GOOGLE_CLIENT_ID: "test-google-client-id" FRONTEND_BASE_URL: "http://localhost:5500/frontend" PIPELINE_TRIGGER_SECRET: "test-pipeline-secret" + RESEND_API_KEY: "test-resend-api-key" - name: Run tests with coverage working-directory: backend @@ -63,6 +64,7 @@ jobs: GOOGLE_CLIENT_ID: "test-google-client-id" FRONTEND_BASE_URL: "http://localhost:5500/frontend" PIPELINE_TRIGGER_SECRET: "test-pipeline-secret" + RESEND_API_KEY: "test-resend-api-key" - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 From 44b825b2a9caa20a4a73de438c87fb24096981d8 Mon Sep 17 00:00:00 2001 From: Ochoa-Stack Date: Sun, 26 Jul 2026 22:48:32 -0600 Subject: [PATCH 4/7] fix(ci): correct env var name from DATABASE_URL to TEST_DATABASE_URL --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 282f7b0..7151bba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,7 @@ jobs: env: FLASK_ENV: "testing" SECRET_KEY: "test-secret-key" - DATABASE_URL: "postgresql://usuario:password@localhost:5432/skillstat_test" + TEST_DATABASE_URL: "postgresql://usuario:password@localhost:5432/skillstat_test" JWT_SECRET_KEY: "test-jwt-secret" GOOGLE_CLIENT_ID: "test-google-client-id" FRONTEND_BASE_URL: "http://localhost:5500/frontend" @@ -59,7 +59,7 @@ jobs: env: FLASK_ENV: "testing" SECRET_KEY: "test-secret-key" - DATABASE_URL: "postgresql://usuario:password@localhost:5432/skillstat_test" + TEST_DATABASE_URL: "postgresql://usuario:password@localhost:5432/skillstat_test" JWT_SECRET_KEY: "test-jwt-secret" GOOGLE_CLIENT_ID: "test-google-client-id" FRONTEND_BASE_URL: "http://localhost:5500/frontend" From d3a2be82e1fb5ff8957f4a23e34ff40e6737ef46 Mon Sep 17 00:00:00 2001 From: Ochoa-Stack Date: Sun, 26 Jul 2026 23:03:45 -0600 Subject: [PATCH 5/7] fix(db): sync categories_id_seq after seed migration missing setval --- ...gories_id_seq_after_seed_without_setval.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 backend/migrations/versions/a1b2c3d4e5f6_fix_sync_categories_id_seq_after_seed_without_setval.py diff --git a/backend/migrations/versions/a1b2c3d4e5f6_fix_sync_categories_id_seq_after_seed_without_setval.py b/backend/migrations/versions/a1b2c3d4e5f6_fix_sync_categories_id_seq_after_seed_without_setval.py new file mode 100644 index 0000000..fd3c623 --- /dev/null +++ b/backend/migrations/versions/a1b2c3d4e5f6_fix_sync_categories_id_seq_after_seed_without_setval.py @@ -0,0 +1,58 @@ +"""fix: sync categories_id_seq after seed without setval + +Revision ID: a1b2c3d4e5f6 +Revises: 9a104cdbdaed +Create Date: 2026-07-27 00:00:00.000000 + +La migración 401c30988716 sembró las categorías base (ids 1-6) con ids +explícitos usando ON CONFLICT DO NOTHING, pero omitió el ajuste de la +secuencia categories_id_seq. Esto hace que la primera inserción orgánica +de una Category (p.ej. en tests de integración) intente reutilizar el +id=1 ya ocupado, produciendo: + + duplicate key value violates unique constraint "categories_pkey" + +Este patrón es idéntico al que afectó a cities y se corrigió en +9a104cdbdaed. Se aplica la misma solución: SELECT setval() con +GREATEST(MAX(id)+1, 7) para que la secuencia arranque por encima del +id más alto ya sembrado (id=6), sin depender de la condición de la tabla. + +La operación es idempotente: si la secuencia ya fue avanzada (p.ej. en +un entorno que ya tenía inserciones orgánicas de categories), GREATEST +garantiza que no la retrocedemos. +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a1b2c3d4e5f6' +down_revision = '9a104cdbdaed' +branch_labels = None +depends_on = None + + +def upgrade(): + # Adelantar la secuencia por encima del id máximo ya sembrado (6), + # para que el próximo INSERT orgánico comience desde id=7 como mínimo. + # GREATEST(MAX(id)+1, 7) es defensivo: si ya existen filas con id > 6, + # usamos ese valor; si la tabla solo tiene las filas sembradas (max=6), + # partimos de 7. + op.execute( + sa.text( + """ + SELECT setval( + pg_get_serial_sequence('categories', 'id'), + GREATEST((SELECT MAX(id) + 1 FROM categories), 7) + ) + """ + ) + ) + + +def downgrade(): + # No hay reversión significativa posible para un ajuste de secuencia: + # retroceder la secuencia podría causar colisiones con filas ya + # insertadas en producción. Se documenta explícitamente como decisión + # de diseño, no como omisión. + pass From 44e75f11de6545b089ed0bb5d2400e9de0593837 Mon Sep 17 00:00:00 2001 From: Ochoa-Stack Date: Sun, 26 Jul 2026 23:03:45 -0600 Subject: [PATCH 6/7] fix(tests): make _make_city idempotent against pre-seeded city id=1 --- .../tests/integration/test_market_trends_service.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/tests/integration/test_market_trends_service.py b/backend/tests/integration/test_market_trends_service.py index 164c4c1..a302f0a 100644 --- a/backend/tests/integration/test_market_trends_service.py +++ b/backend/tests/integration/test_market_trends_service.py @@ -18,7 +18,17 @@ # Duplicados deliberadamente aqui (principio DAMP): cada archivo de tests es autocontenido. No se importa desde otros archivos de tests para preservar aislamiento y legibilidad individual. def _make_city(db_session, city_id, name="Mexico Nacional", state="Nacional"): - """ Crea una City con un ID especifico usando INSERT directo para poder controlar el id=1 que necesita el fallback de generate_snapshots(). Usa INSERT con id explicito en lugar de add() para garantizar el id exacto, ya que PostgreSQL asigna secuencias y podria saltarse el 1 si ya hubo inserts previos en la sesion """ + """ Crea una City con un ID especifico usando INSERT directo para poder + controlar el id=1 que necesita el fallback de generate_snapshots(). Usa + INSERT con id explicito en lugar de add() para garantizar el id exacto. + + Si ya existe una City con ese id (p.ej. la fila sembrada por la migración + 9a104cdbdaed para México Nacional), la retorna directamente sin intentar + re-insertarla. Esto hace la función idempotente frente al esquema base + que ya contiene id=1 tras flask db upgrade. """ + existing = db_session.get(City, city_id) + if existing: + return existing city = City(id=city_id, name=name, state=state) db_session.add(city) db_session.flush() From 461127e51d288242d3862d6488c9aac8507f70c7 Mon Sep 17 00:00:00 2001 From: Ochoa-Stack Date: Sun, 26 Jul 2026 23:03:46 -0600 Subject: [PATCH 7/7] ci(tests): install postgresql-client-18 to match Neon server version --- .github/workflows/tests.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7151bba..bbfc2fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,6 +53,16 @@ jobs: PIPELINE_TRIGGER_SECRET: "test-pipeline-secret" RESEND_API_KEY: "test-resend-api-key" + - name: Install PostgreSQL 18 client tools + run: | + sudo apt-get install -y curl ca-certificates + sudo install -d /usr/share/postgresql-common/pgdg + sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc + sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + sudo apt-get update + sudo apt-get install -y postgresql-client-18 + pg_dump --version + - name: Run tests with coverage working-directory: backend run: pytest --cov=app --cov-report=xml