Skip to content
84 changes: 84 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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"
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"
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
env:
FLASK_ENV: "testing"
SECRET_KEY: "test-secret-key"
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"
PIPELINE_TRIGGER_SECRET: "test-pipeline-secret"
RESEND_API_KEY: "test-resend-api-key"

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: backend/coverage.xml
fail_ci_if_error: false
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 11 additions & 1 deletion backend/tests/integration/test_market_trends_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading