Módulo 8: Proyecto Integrador

Fase 1: Code Review — El Codebase de TaskFlow API

Fase 1: Code Review — El Codebase de TaskFlow API

Descripción de la cápsula

Aquí está el codebase completo de TaskFlow API. Estos son los archivos que debes revisar, con el código exacto que examinarás durante el proyecto. El codebase contiene entre 15 y 20 problemas distribuidos en las 5 categorías que viste en la cápsula anterior: hallucinations, security holes, edge cases no manejados, lógica incorrecta, y runtime bugs.

Tu trabajo en esta fase es aplicar el checklist de code review del módulo 4, comparar cada archivo contra los requisitos funcionales, y documentar cada problema que encuentres en el formato del findings document.

No corrijas nada todavía. Esta fase es de observación y documentación. Las correcciones vienen en la Cápsula 04.


El Codebase Completo

Archivo 1: requirements.txt

fastapi==0.115.0
uvicorn==0.30.0
pyjwt==2.9.0
bcrypt==4.2.0
pydantic==2.9.0
python-multipart==0.0.12

Archivo 2: config.py

import os


class Settings:
    APP_NAME: str = "TaskFlow API"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = True

    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./taskflow.db")
    DATABASE_PATH: str = "taskflow.db"

    JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "super-secret-key-taskflow-2026")
    JWT_ALGORITHM: str = "HS256"
    JWT_EXPIRATION_MINUTES: int = 30

    BCRYPT_ROUNDS: int = 12

    DEFAULT_PAGE_SIZE: int = 10
    MAX_PAGE_SIZE: int = 100


settings = Settings()

Archivo 3: database.py

import sqlite3
from contextlib import contextmanager

from config import settings


def init_db():
    conn = sqlite3.connect(settings.DATABASE_PATH)
    cursor = conn.cursor()

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT UNIQUE NOT NULL,
            name TEXT NOT NULL,
            password TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            description TEXT DEFAULT '',
            priority TEXT DEFAULT 'medium',
            status TEXT DEFAULT 'pending',
            user_id INTEGER NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (user_id) REFERENCES users (id)
        )
    """)

    conn.commit()
    conn.close()


@contextmanager
def get_db():
    conn = sqlite3.connect(settings.DATABASE_PATH)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
    finally:
        conn.close()


def execute_query(query: str, params: tuple = ()) -> list:
    with get_db() as conn:
        cursor = conn.cursor()
        cursor.execute(query, params)
        conn.commit()
        return cursor.fetchall()


def execute_insert(query: str, params: tuple = ()) -> int:
    with get_db() as conn:
        cursor = conn.cursor()
        cursor.execute(query, params)
        conn.commit()
        return cursor.lastrowid

Archivo 4: models.py

from datetime import datetime
from typing import Optional

from pydantic import BaseModel, EmailStr, field_validator


class UserCreate(BaseModel):
    email: EmailStr
    name: str
    password: str

    @field_validator("password")
    @classmethod
    def validate_password(cls, v):
        if len(v) < 4:
            raise ValueError("Password must be at least 4 characters")
        return v


class UserResponse(BaseModel):
    id: int
    email: str
    name: str
    created_at: str


class LoginRequest(BaseModel):
    email: str
    password: str


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"


class TaskCreate(BaseModel):
    title: str
    description: Optional[str] = ""
    priority: str = "medium"
    status: str = "pending"

    @field_validator("title")
    @classmethod
    def validate_title(cls, v):
        if len(v) < 1:
            raise ValueError("Title cannot be empty")
        return v


class TaskUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    priority: Optional[str] = None
    status: Optional[str] = None


class TaskResponse(BaseModel):
    id: int
    title: str
    description: str
    priority: str
    status: str
    user_id: int
    created_at: str
    updated_at: str


class TaskListResponse(BaseModel):
    tasks: list[TaskResponse]
    total: int
    page: int
    size: int


class StatsResponse(BaseModel):
    total_tasks: int
    by_status: dict
    by_priority: dict
    completion_percentage: float

Archivo 5: services/auth_service.py

import jwt
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Header, HTTPException

from bcrypt import hashpw, gensalt, checkpw, verify_hash

from config import settings


def hash_password(password: str) -> str:
    salt = gensalt(rounds=settings.BCRYPT_ROUNDS)
    return hashpw(password.encode("utf-8"), salt).decode("utf-8")


def verify_password(plain_password: str, hashed_password: str) -> bool:
    return checkpw(
        plain_password.encode("utf-8"),
        hashed_password.encode("utf-8")
    )


def create_access_token(user_id: int, email: str) -> str:
    expire = datetime.utcnow() + timedelta(
        minutes=settings.JWT_EXPIRATION_MINUTES
    )
    payload = {
        "sub": str(user_id),
        "email": email,
        "exp": expire,
        "iat": datetime.utcnow()
    }
    return jwt.encode(
        payload,
        settings.JWT_SECRET_KEY,
        algorithm=settings.JWT_ALGORITHM
    )


def decode_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            settings.JWT_SECRET_KEY,
            algorithms=[settings.JWT_ALGORITHM]
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token has expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")


async def get_current_user(authorization: str = Header(...)) -> dict:
    if not authorization.startswith("Bearer "):
        raise HTTPException(
            status_code=401,
            detail="Invalid authorization header"
        )
    token = authorization.split(" ")[1]
    payload = decode_token(token)
    return {
        "user_id": int(payload["sub"]),
        "email": payload["email"]
    }

Archivo 6: services/task_service.py

from datetime import datetime
from typing import Optional

from database import execute_query, execute_insert, get_db
from models import TaskCreate, TaskUpdate


def create_task(task: TaskCreate, user_id: int) -> dict:
    query = """
        INSERT INTO tasks (title, description, priority, status, user_id)
        VALUES (?, ?, ?, ?, ?)
    """
    task_id = execute_insert(
        query,
        (task.title, task.description, task.priority, task.status, user_id)
    )
    return get_task_by_id(task_id, user_id)


def get_task_by_id(task_id: int, user_id: int) -> Optional[dict]:
    query = """
        SELECT id, title, description, priority, status, 
               user_id, created_at, updated_at
        FROM tasks WHERE id = ?
    """
    rows = execute_query(query, (task_id,))
    if not rows:
        return None
    task = dict(rows[0])
    return task


def get_tasks(
    user_id: int,
    status: Optional[str] = None,
    priority: Optional[str] = None,
    page: int = 1,
    size: int = 10,
) -> dict:
    base_query = "SELECT * FROM tasks WHERE user_id = ?"
    count_query = "SELECT COUNT(*) as total FROM tasks WHERE user_id = ?"
    params = [user_id]

    if status:
        base_query += f" AND status = '{status}'"
        count_query += f" AND status = '{status}'"

    if priority:
        base_query += f" AND priority = '{priority}'"
        count_query += f" AND priority = '{priority}'"

    offset = page * size
    base_query += f" ORDER BY created_at DESC LIMIT {size} OFFSET {offset}"

    with get_db() as conn:
        cursor = conn.cursor()

        cursor.execute(count_query, tuple(params))
        total = cursor.fetchone()["total"]

        cursor.execute(base_query, tuple(params))
        rows = cursor.fetchall(as_dict=True)

    tasks = [dict(row) for row in rows]
    return {
        "tasks": tasks,
        "total": total,
        "page": page,
        "size": size,
    }


def update_task(task_id: int, task_update: TaskUpdate, user_id: int) -> Optional[dict]:
    existing = get_task_by_id(task_id, user_id)
    if not existing:
        return None

    fields = []
    values = []

    if task_update.title is not None:
        fields.append("title = ?")
        values.append(task_update.title)
    if task_update.description is not None:
        fields.append("description = ?")
        values.append(task_update.description)
    if task_update.priority is not None:
        fields.append("priority = ?")
        values.append(task_update.priority)
    if task_update.status is not None:
        fields.append("status = ?")
        values.append(task_update.status)

    if not fields:
        return existing

    fields.append("updated_at = ?")
    values.append(datetime.utcnow().isoformat())
    values.append(task_id)

    query = f"UPDATE tasks SET {', '.join(fields)} WHERE id = ?"
    execute_query(query, tuple(values))

    return get_task_by_id(task_id, user_id)


def delete_task(task_id: int, user_id: int) -> bool:
    existing = get_task_by_id(task_id, user_id)
    if not existing:
        return False

    query = "DELETE FROM tasks WHERE id = ?"
    execute_query(query, (task_id,))
    return True


def get_user_stats(user_id: int) -> dict:
    query = "SELECT status, priority FROM tasks WHERE user_id = ?"
    rows = execute_query(query, (user_id,))

    total = len(rows)
    by_status = {}
    by_priority = {}

    for row in rows:
        row_dict = dict(row)
        status = row_dict["status"]
        priority = row_dict["priority"]

        by_status[status] = by_status.get(status, 0) + 1
        by_priority[priority] = by_priority.get(priority, 0) + 1

    completion_percentage = (
        by_status.get("completed", 0) / total * 100
    )

    return {
        "total_tasks": total,
        "by_status": by_status,
        "by_priority": by_priority,
        "completion_percentage": round(completion_percentage, 2),
    }

Archivo 7: routes/auth.py

from fastapi import APIRouter, HTTPException

from models import UserCreate, UserResponse, LoginRequest, TokenResponse
from services.auth_service import hash_password, verify_password, create_access_token
from database import execute_query, execute_insert

router = APIRouter(prefix="/auth", tags=["Authentication"])


@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate):
    existing = execute_query(
        "SELECT id FROM users WHERE email = ?", (user.email,)
    )
    if existing:
        raise HTTPException(status_code=400, detail="Email already registered")

    user_id = execute_insert(
        "INSERT INTO users (email, name, password) VALUES (?, ?, ?)",
        (user.email, user.name, user.password),
    )

    created_user = execute_query(
        "SELECT id, email, name, created_at FROM users WHERE id = ?",
        (user_id,),
    )

    row = dict(created_user[0])
    return UserResponse(**row)


@router.post("/login", response_model=TokenResponse)
async def login(credentials: LoginRequest):
    users = execute_query(
        "SELECT id, email, password FROM users WHERE email = ?",
        (credentials.email,),
    )

    if not users:
        raise HTTPException(status_code=401, detail="Invalid credentials")

    user = dict(users[0])

    if not verify_password(credentials.password, user["password"]):
        raise HTTPException(status_code=401, detail="Invalid credentials")

    token = create_access_token(user["id"], user["email"])
    return TokenResponse(access_token=token)

Archivo 8: routes/tasks.py

from typing import Optional

from fastapi import APIRouter, HTTPException, Depends, Query

from models import TaskCreate, TaskUpdate, TaskResponse, TaskListResponse
from services.auth_service import get_current_user
from services.task_service import (
    create_task,
    get_task_by_id,
    get_tasks,
    update_task,
    delete_task,
)

router = APIRouter(prefix="/tasks", tags=["Tasks"])


@router.post("/", response_model=TaskResponse)
async def create_new_task(
    task: TaskCreate,
    current_user: dict = Depends(get_current_user),
):
    created = create_task(task, current_user["user_id"])
    return TaskResponse(**created)


@router.get("/", response_model=TaskListResponse)
async def list_tasks(
    status: Optional[str] = Query(default=None),
    priority: Optional[str] = Query(default=None),
    page: int = Query(default=1, ge=0),
    size: int = Query(default=10, ge=1, le=100),
    current_user: dict = Depends(get_current_user),
):
    result = get_tasks(
        user_id=current_user["user_id"],
        status=status,
        priority=priority,
        page=page,
        size=size,
    )
    return TaskListResponse(**result)


@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(
    task_id: int,
    current_user: dict = Depends(get_current_user),
):
    task = get_task_by_id(task_id, current_user["user_id"])
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    return TaskResponse(**task)


@router.put("/{task_id}", response_model=TaskResponse)
async def update_existing_task(
    task_id: int,
    task_update: TaskUpdate,
    current_user: dict = Depends(get_current_user),
):
    updated = update_task(task_id, task_update, current_user["user_id"])
    if not updated:
        raise HTTPException(status_code=404, detail="Task not found")
    return TaskResponse(**updated)


@router.delete("/{task_id}")
async def delete_existing_task(
    task_id: int,
    current_user: dict = Depends(get_current_user),
):
    deleted = delete_task(task_id, current_user["user_id"])
    if not deleted:
        raise HTTPException(status_code=404, detail="Task not found")
    return {"message": "Task deleted successfully"}

Archivo 9: routes/users.py

from fastapi import APIRouter, HTTPException, Depends

from models import StatsResponse
from services.auth_service import get_current_user
from services.task_service import get_user_stats
from database import execute_query

router = APIRouter(prefix="/users", tags=["Users"])


@router.get("/me")
async def get_profile(current_user: dict = Depends(get_current_user)):
    users = execute_query(
        "SELECT id, email, name, created_at FROM users WHERE id = ?",
        (current_user["user_id"],),
    )
    if not users:
        raise HTTPException(status_code=404, detail="User not found")
    return dict(users[0])


@router.get("/stats", response_model=StatsResponse)
async def get_stats(current_user: dict = Depends(get_current_user)):
    stats = get_user_stats(current_user["user_id"])
    return StatsResponse(**stats)


@router.get("/search")
async def search_users(query: str):
    sql = f"SELECT id, email, name FROM users WHERE name LIKE '%{query}%'"
    results = execute_query(sql)
    return [dict(row) for row in results]

Archivo 10: main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic_settings import BaseSettings

from config import settings
from database import init_db
from routes import auth, tasks, users

app = FastAPI(
    title=settings.APP_NAME,
    version=settings.APP_VERSION,
    debug=settings.DEBUG,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.on_event("startup")
async def startup():
    init_db()


app.include_router(auth.router)
app.include_router(tasks.router)
app.include_router(users.router)


@app.get("/health")
async def health_check():
    return {
        "status": "healthy",
        "app": settings.APP_NAME,
        "version": settings.APP_VERSION,
    }

Instrucciones para el Code Review

Paso 1: Lectura inicial (15 minutos)

Lee todos los archivos de arriba en orden. No busques bugs todavía — tu objetivo es entender:

  1. ¿Cómo se conectan los archivos entre sí?
  2. ¿Qué flujo sigue un request desde que llega hasta que se responde?
  3. ¿Qué dependencias externas usa el proyecto?
  4. ¿Cuál es la estructura general de datos (tablas, modelos, responses)?

Responde estas preguntas mentalmente antes de continuar:

  • ¿Cómo se autentica un usuario? ¿Qué flujo sigue un login?
  • ¿Cómo se crea una tarea? ¿Qué validaciones pasa?
  • ¿Cómo se protegen los endpoints? ¿Todos están protegidos?
  • ¿Dónde se guardan las contraseñas? ¿Cómo se verifica una contraseña?
  • ¿Cómo funciona la paginación? ¿Qué pasa en la primera página?

Paso 2: Review contra requisitos funcionales (30-45 minutos)

Abre los requisitos funcionales de la Cápsula 01 y compara cada requisito contra la implementación:

Checklist de requisitos

## Review contra Requisitos

### RF-01: Gestión de Usuarios
- [ ] RF-01.1: ¿Se puede registrar con email, nombre y contraseña?
- [ ] RF-01.2: ¿La contraseña se almacena hasheada con bcrypt?
- [ ] RF-01.3: ¿El email es único?
- [ ] RF-01.4: ¿Se valida formato de email y contraseña >= 8 caracteres?

### RF-02: Autenticación
- [ ] RF-02.1: ¿Login retorna JWT token?
- [ ] RF-02.2: ¿El JWT expira en 30 minutos?
- [ ] RF-02.3: ¿El JWT secret viene de variable de entorno sin fallback?
- [ ] RF-02.4: ¿Todos los endpoints protegidos requieren token?

### RF-03: Gestión de Tareas
- [ ] RF-03.1: ¿Se puede crear tarea con los campos correctos?
- [ ] RF-03.2: ¿La paginación funciona correctamente (10 por página)?
- [ ] RF-03.3: ¿Se puede filtrar por estado y prioridad?
- [ ] RF-03.4: ¿Se puede obtener detalle solo si pertenece al usuario?
- [ ] RF-03.5: ¿Solo el propietario puede actualizar?
- [ ] RF-03.6: ¿Delete es soft delete (cambiar estado a "deleted")?
- [ ] RF-03.7: ¿Un usuario no puede ver/editar/eliminar tareas de otro?

### RF-04: Estadísticas
- [ ] RF-04.1: ¿Retorna total, por estado, por prioridad?
- [ ] RF-04.2: ¿Se calculan solo sobre tareas activas (no deleted)?
- [ ] RF-04.3: ¿Incluye porcentaje de completadas sobre total activo?

### RF-05: Validaciones
- [ ] RF-05.1: ¿Título entre 3 y 100 caracteres?
- [ ] RF-05.2: ¿Prioridad solo low/medium/high?
- [ ] RF-05.3: ¿Estado solo pending/in_progress/completed?
- [ ] RF-05.4: ¿Page >= 1 y size entre 1 y 100?
- [ ] RF-05.5: ¿ID inexistente retorna 404?
- [ ] RF-05.6: ¿Tarea de otro usuario retorna 403, no 404?

### RF-06: No Funcionales
- [ ] RF-06.1: ¿Config sensible desde variables de entorno?
- [ ] RF-06.2: ¿Queries SQL usan parámetros, no concatenación?
- [ ] RF-06.3: ¿Errores internos no exponen stack traces?
- [ ] RF-06.4: ¿Respuestas consistentes en formato JSON?

Para cada requisito que falla, documenta inmediatamente en tu findings document.

Paso 3: Checklist de seguridad del Módulo 4 (15-20 minutos)

Aplica los primeros 5 items del checklist de seguridad del Módulo 4 a cada archivo:

Items de seguridad a verificar

Item 1: No hay secrets hardcoded

Busca en cada archivo:

  • Strings que parezcan API keys, passwords, o tokens
  • os.getenv() con fallback que contenga valores reales
  • Connection strings con credenciales

Item 2: No hay SQL injection

Busca en cada archivo que interactúe con la base de datos:

  • f-strings dentro de queries SQL
  • Concatenación de strings en queries
  • Variables de usuario insertadas directamente en el SQL

Item 3: Autenticación en todos los endpoints sensibles

Revisa cada endpoint:

  • ¿Tiene Depends(get_current_user) o equivalente?
  • ¿Hay endpoints que acceden a datos pero no verifican identidad?

Item 4: Contraseñas almacenadas correctamente

Verifica el flujo completo de password:

  • ¿Se hashea antes de guardar en la base de datos?
  • ¿Se compara correctamente durante el login?
  • ¿En algún momento se almacena o transmite en texto plano?

Item 5: Permisos verificados correctamente

Para cada operación sobre un recurso:

  • ¿Se verifica que el usuario actual es el propietario?
  • ¿Qué pasa si un usuario intenta acceder a un recurso de otro?

Paso 4: Búsqueda de hallucinations (10-15 minutos)

Aplica las técnicas del Módulo 3:

Para cada import del codebase:
1. ¿El paquete existe en requirements.txt?
2. ¿El módulo/clase importado existe en ese paquete?
3. ¿La versión en requirements.txt tiene esa API?
Para cada función de librería externa:
1. ¿Los parámetros que se pasan son correctos?
2. ¿El tipo de retorno es el esperado?
3. ¿La función existe en la versión instalada?

Presta especial atención a:

  • Imports que no se usan en el archivo
  • Imports de módulos o funciones que no existen en el paquete
  • Uso de APIs con signatures incorrectas

Paso 5: Búsqueda de patrones de error (10-15 minutos)

Del Módulo 5, busca activamente:

Edge cases no manejados:

  • ¿Qué pasa cuando una lista está vacía?
  • ¿Qué pasa con valores None/null donde no se esperan?
  • ¿Los límites de paginación son correctos?

Lógica incorrecta:

  • ¿Los filtros incluyen/excluyen lo correcto?
  • ¿Los cálculos producen resultados correctos?
  • ¿Las validaciones son suficientes?

Runtime bugs:

  • ¿Hay operaciones que fallarían con ciertos tipos de datos?
  • ¿Hay divisiones que podrían ser por cero?
  • ¿Hay accesos a diccionario que podrían fallar?

Template para Documentar Findings

Usa este template para registrar cada problema que encuentres. Crea un archivo markdown y agrega una fila por cada finding:

# TaskFlow API — Findings Document

## Información del Reviewer
- **Nombre:** [Tu nombre]
- **Fecha:** [Fecha del review]
- **Tiempo invertido en code review:** [Tiempo]

## Resumen Ejecutivo

[2-3 oraciones describiendo el estado general del codebase.
Ejemplo: "El codebase implementa la funcionalidad básica pero contiene 
múltiples vulnerabilidades de seguridad críticas y varios problemas de 
lógica de negocio que no cumplen con los requisitos funcionales."]

## Findings

| # | Severidad | Categoría | Archivo | Línea(s) | Descripción | Requisito Violado | Impacto |
|---|-----------|-----------|---------|----------|-------------|-------------------|---------|
| 1 |  |  |  |  |  |  |  |
| 2 |  |  |  |  |  |  |  |
| 3 |  |  |  |  |  |  |  |

## Estadísticas de Findings

| Severidad | Cantidad |
|-----------|----------|
| Critical  |          |
| High      |          |
| Medium    |          |
| Low       |          |
| **Total** |          |

## Notas del Reviewer

[Observaciones generales sobre el codebase, patrones que notaste,
áreas que requieren atención especial]

Cómo llenar cada columna

Severidad:

  • Critical: Explotable en producción, puede causar pérdida de datos o acceso no autorizado
  • High: Funcionalidad incorrecta que afecta a usuarios
  • Medium: Edge case o problema de robustez
  • Low: Problema de calidad o mantenibilidad

Categoría:

  • Security — Vulnerabilidad de seguridad
  • Hallucination — Import/API/función que no existe
  • Logic — Lógica de negocio incorrecta
  • Edge Case — Input no manejado que causa error
  • Runtime — Error que solo aparece al ejecutar

Archivo: Nombre del archivo donde encontraste el problema.

Línea(s): Número(s) de línea aproximado(s) en el código de la cápsula.

Descripción: Qué es el problema en una oración clara.

Requisito Violado: El ID del requisito funcional que no se cumple (e.g., RF-01.2, RF-06.2).

Impacto: Qué consecuencia tiene el problema en producción.


Guía de Priorización del Review

No revises los archivos de forma aleatoria. Sigue esta secuencia de prioridad basada en la pirámide del Módulo 4:

Prioridad 1: Seguridad (revisar primero)

ArchivoQué buscar
config.pySecrets hardcoded, configuración insegura
services/auth_service.pyManejo de contraseñas, generación de tokens
routes/auth.pyFlujo de registro y login
database.pySQL injection en queries

Prioridad 2: Lógica de negocio

ArchivoQué buscar
services/task_service.pyLógica de CRUD, filtros, estadísticas
routes/tasks.pyValidaciones, permisos, respuestas correctas
routes/users.pyEstadísticas, búsqueda

Prioridad 3: Robustez y edge cases

ArchivoQué buscar
models.pyValidaciones suficientes, constraints correctos
routes/tasks.pyPaginación, filtros con valores inesperados
services/task_service.pyDivisión por cero, listas vacías

Prioridad 4: Calidad y hallucinations

ArchivoQué buscar
main.pyImports correctos, configuración coherente
Todos los archivosImports que no existen, APIs con parámetros incorrectos

Consejos para el Code Review

Consejo 1: Lee el código como si fuera código de producción

No leas pensando "esto es un ejercicio." Lee pensando "esto va a producción mañana." Los problemas que dejas pasar los encontrarán tus usuarios.

Consejo 2: Sigue el flujo de datos

Para cada endpoint, sigue el camino completo:

Request → Route → Service → Database → Response

Los problemas suelen esconderse en las transiciones entre capas. Una validación en el modelo no sirve si la ruta no la usa. Una query correcta no sirve si el service interpreta mal los resultados.

Consejo 3: Pregunta "¿Qué pasa si...?"

Para cada función, hazte estas preguntas:

  • ¿Qué pasa si el input es vacío?
  • ¿Qué pasa si el input es None?
  • ¿Qué pasa si el usuario no tiene datos?
  • ¿Qué pasa si el ID no existe?
  • ¿Qué pasa si un atacante manipula el input?

Consejo 4: No te detengas en el primer problema

Es tentador encontrar un problema y empezar a corregirlo. Resiste. Completa el review completo primero. Las correcciones vienen después.

Consejo 5: Usa Claude Code estratégicamente

Puedes usar Claude Code durante el review para:

Uso correcto:
"¿Existe la clase BaseSettings en pydantic_settings? 
¿Está en el paquete pydantic-settings o en pydantic?"

"¿Los parámetros de jwt.encode en PyJWT 2.9 son correctos?"

"¿sqlite3.Row soporta dict() directamente?"
Uso incorrecto:
"Encuentra todos los bugs en este código"
"¿Este código tiene problemas de seguridad?"

La diferencia: preguntas específicas y verificables vs preguntas abiertas que delegan tu trabajo.

Consejo 6: Documenta mientras revisas

No confíes en tu memoria. Cada vez que encuentres algo sospechoso, agrégalo inmediatamente al findings document aunque no estés 100% seguro. Es más fácil descartar un finding falso que recordar uno que no anotaste.


Errores Comunes en Esta Fase

Error 1: Solo buscar bugs de un tipo

Los developers con background de seguridad encuentran todos los security holes pero ignoran la lógica incorrecta. Los que son fuertes en lógica de negocio encuentran los filtros mal implementados pero pasan por alto el SQL injection. Fuerza una revisión por cada categoría.

Error 2: Asumir que los imports están bien

"Si el código se escribió, los imports deben existir." No necesariamente. AI genera imports de paquetes que existen pero funciones que no. O de paquetes que se llaman casi igual. Verifica cada import externo.

Error 3: No leer los requisitos con atención

Algunos problemas solo son visibles si conoces el requisito. "La contraseña debe ser de al menos 8 caracteres" — si el código valida 4, es un bug que solo puedes encontrar leyendo ambos documentos.

Error 4: Confundir "funciona" con "correcto"

Que un endpoint devuelva un 200 no significa que sea correcto. ¿Devuelve los datos correctos? ¿Para todos los usuarios? ¿Con todos los inputs? "Funciona" es un bar bajísimo. "Correcto según los requisitos" es lo que buscas.

Error 5: No documentar con suficiente detalle

"Hay un problema en task_service.py" no es un finding útil. "En task_service.py línea 45, la función get_tasks usa f-strings para insertar el parámetro status en la query SQL, lo cual es vulnerable a SQL injection (RF-06.2)" sí lo es.


Checkpoint del Code Review

Antes de pasar a la Fase 2 (Debugging), verifica:

  • ✅ Leíste todos los archivos del codebase completo
  • ✅ Comparaste cada requisito funcional contra la implementación
  • ✅ Aplicaste el checklist de seguridad a los archivos relevantes
  • ✅ Buscaste hallucinations en todos los imports y APIs externas
  • ✅ Buscaste edge cases no manejados en cada función
  • ✅ Tienes un findings document con al menos 10 problemas documentados
  • ✅ Cada finding tiene: severidad, categoría, archivo, línea, descripción, impacto
  • ✅ No has empezado a corregir nada todavía

Si tienes menos de 10 findings, haz otra pasada. El codebase tiene entre 15 y 20 problemas. Si encontraste 7, te faltan al menos 8 más. Revisa las categorías donde tienes menos findings — probablemente te falta buscar en esa dirección.


Mapa de Verificación por Archivo

Para ayudarte a ser sistemático, usa este mapa que indica qué verificar en cada archivo. No es exhaustivo — es un punto de partida:

config.py

  • ¿Los valores sensibles tienen fallback hardcoded?
  • ¿La configuración de debug es apropiada para producción?

database.py

  • ¿Todas las queries usan parámetros?
  • ¿Las conexiones se cierran correctamente?

models.py

  • ¿Las validaciones coinciden con los requisitos?
  • ¿Hay validaciones faltantes?
  • ¿Los tipos son correctos?

services/auth_service.py

  • ¿La contraseña se hashea antes de almacenar?
  • ¿La verificación de password es correcta?
  • ¿El token se genera y decodifica correctamente?
  • ¿Los imports son correctos y existen?

services/task_service.py

  • ¿Las queries usan parámetros?
  • ¿Los filtros funcionan correctamente?
  • ¿La paginación calcula offset correctamente?
  • ¿Las estadísticas excluyen tareas eliminadas?
  • ¿Hay división por cero posible?
  • ¿Se verifica la propiedad de la tarea?

routes/auth.py

  • ¿La contraseña se hashea antes de insertar en la DB?
  • ¿La verificación de password en login es correcta?

routes/tasks.py

  • ¿Todos los endpoints verifican autenticación?
  • ¿Las validaciones de paginación son correctas?
  • ¿Se verifica propiedad de tarea en todos los endpoints?
  • ¿El delete implementa soft delete?

routes/users.py

  • ¿Todos los endpoints requieren autenticación?
  • ¿Hay SQL injection en alguna query?

main.py

  • ¿Los imports son correctos y existen?
  • ¿La configuración de CORS es apropiada?

Siguiente cápsula: Fase de Debugging — Cómo configurar y ejecutar el codebase localmente, y debuggear los runtime bugs encontrados durante el review.


Debugging & Code Review with Claude Code — Módulo 8, Cápsula 02 Claude Code Agentic Development Path — Guía #6 de 11