El shell del editor de posts, un bug de token vencido, y sin gema de mocking
Me propuse construir el shell del nuevo editor de posts de blog-manager, /posts/:id/edit, una tabla de borradores respaldada por base de datos, autoguardado, y una acción de descarte. Es el primero de un pequeño grupo de issues del editor (vista previa, serializador de frontmatter, tags, traducción, panel de imagen destacada, flujo de commit) que llevaban tiempo en el backlog sin orden de dependencias entre ellos, así que antes de tocar código tuve que averiguar cuál estaba realmente desbloqueado.
Lo que construí
Una tabla editor_drafts (post_id, locale, frontmatter, body, base_file_sha) y un modelo EditorDraft. El frontmatter es un Hash serializado como JSON, siguiendo el mismo patrón serialize ..., coder: JSON que ya usa la columna Post#tags, SQLite todavía no tiene ningún precedente de t.json en esta app, y no quería ser quien introdujera una convención nueva de tipo de columna en un issue fundacional.
PostEditor::DraftLoader, un servicio pequeño que devuelve un borrador existente o construye uno nuevo trayendo el archivo actual desde GitHub y separándolo en frontmatter/body. Copia casi textual el fallback de ruta en/-y-luego-plana del committer de imagen destacada, el patrón ya estaba ahí, sin motivo para inventar uno nuevo.
Además de eso: PostsController#edit/#update_draft/#discard_draft, una vista con un <textarea> simple con paneles provisionales para lo que issues posteriores van a completar, y un controlador de Stimulus para el autoguardado (con debounce de ~1.5s, Tab inserta dos espacios en vez de mover el foco, Cmd/Ctrl-S guarda de inmediato).
Decisiones y por qué
GET /posts/:id/edit es la primera acción de controlador en esta app que llama a GitHub de forma síncrona. Todas las llamadas a GitHub existentes, el escaneo, los commits de imagen destacada, corren dentro de un job en segundo plano. Dudé bastante con esto, pero leer un solo archivo al cargar la página es lo bastante chico y rápido como para que una interfaz de job-más-polling fuera más maquinaria de la que el problema necesita; los jobs se justifican para escrituras (que necesitan reintentos y manejo de errores) y lecturas masivas (el escaneo), no para un GET puntual. Sí tuve que inventar una convención de rescate para Github::ContentClient::Error a nivel de controlador, porque no existía nada parecido para copiar.
Lo que me sorprendió
Dos cosas que solo detecté porque fui y probé la página en un navegador de verdad en vez de confiar en las pruebas en verde:
-
Mi primera versión de la acción
editsiempre construía unGithub::ContentClientantes de revisar si ya existía un borrador, lo que significaba que reabrir un borrador ya abierto explotaba conAuthErroren el momento en que el token de un blog quedaba vencido o faltante, aunque esa solicitud no necesitara GitHub para nada. Solución fácil una vez que lo vi (revisar primero si ya existe un borrador, y construir el cliente solo si en verdad hay que traer algo), pero es exactamente el tipo de bug que una suite de pruebas con todo mockeado pasa por alto sin verlo. -
Minitest 6 separó
minitest/mockdel núcleo sin hacer mucho ruido,Object#stubno está disponible a menos que se incluya la gema separada, y ni siquiera viene incluida en esta configuración de Ruby. Como esta app tampoco tiene Mocha ni WebMock, armé a mano un helper de stub condefine_singleton_method/remove_methoden vez de sumar una dependencia nueva.
Tampoco tenía un script de verificación empaquetado para apoyarme, así que verificar de punta a punta significó escribir a mano una sesión real de login-más-editor con Playwright (esta app no tiene ningún package.json, importmap-rails, sin paso de build de Node) y correr un escaneo de axe-core contra la página nueva. Ese escaneo encontró un descuido real: la página del editor no tenía ningún <h1>, algo que la página de vista sí obtiene gratis del encabezado del título del post, pero mi nuevo layout no.
Antes de todo esto había repasado los otros cinco issues del editor y encontré que todos se habían registrado con dependencias entre sí que no se reflejaban en su estado, el issue de flujo de commit en particular decía apoyarse en una tabla de borradores y un serializador de frontmatter que todavía no existían. Agregué un estado Blocked al proyecto, moví todo lo que estaba realmente bloqueado ahí con un comentario que nombraba el bloqueo, y dejé este como el único ítem desbloqueado, lo cual hizo que “empezar por acá” fuera una decisión fácil.
Lo que sigue
El serializador de frontmatter y el resto del grupo del editor siguen en la fila, ahora que existen el shell y la tabla de borradores de los que dependen.
Lecturas relacionadas
Escribir pruebas que verifican lo correcto
Dos vacíos de cobertura cerrados, y la pregunta más útil que hizo después una revisión: no "esto pasa" sino "qué atraparía en realidad esta prueba".
Qué pasa cuando un job transmite y nadie escucha
Cerrando el ciclo de la imagen destacada: frontmatter que solo inserta, una validación que detectó drift real, y un broadcast sin oyentes.
Una corrección de desfase de documentación que no fue tan aburrida como sonaba
Tres puntos de auditoría que se volvieron algo más: una afirmación a medio corregir, un restablecimiento de contraseña muerto en silencio, y un correo de staging que enlazaba a producción.
También te podría ser útil
NordPass
Gestor de contraseñas del equipo detrás de NordVPN, con un plan gratuito.
Como afiliado de NordPass, obtengo ingresos por las compras que califican.
Más informaciónProton Drive
Almacenamiento en la nube cifrado, del equipo detrás de Proton Mail.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informacióneSIM Airalo
eSIM de datos local para viajes - sin necesidad de cambiar una SIM física.
Este es mi enlace de referido de Airalo. Obtienes un descuento en tu primer eSIM y yo obtengo crédito de Airalo para el mío.
Más información