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:
- ¿Cómo se conectan los archivos entre sí?
- ¿Qué flujo sigue un request desde que llega hasta que se responde?
- ¿Qué dependencias externas usa el proyecto?
- ¿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 seguridadHallucination— Import/API/función que no existeLogic— Lógica de negocio incorrectaEdge Case— Input no manejado que causa errorRuntime— 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)
| Archivo | Qué buscar |
|---|---|
config.py | Secrets hardcoded, configuración insegura |
services/auth_service.py | Manejo de contraseñas, generación de tokens |
routes/auth.py | Flujo de registro y login |
database.py | SQL injection en queries |
Prioridad 2: Lógica de negocio
| Archivo | Qué buscar |
|---|---|
services/task_service.py | Lógica de CRUD, filtros, estadísticas |
routes/tasks.py | Validaciones, permisos, respuestas correctas |
routes/users.py | Estadísticas, búsqueda |
Prioridad 3: Robustez y edge cases
| Archivo | Qué buscar |
|---|---|
models.py | Validaciones suficientes, constraints correctos |
routes/tasks.py | Paginación, filtros con valores inesperados |
services/task_service.py | División por cero, listas vacías |
Prioridad 4: Calidad y hallucinations
| Archivo | Qué buscar |
|---|---|
main.py | Imports correctos, configuración coherente |
| Todos los archivos | Imports 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