Saltar al contenido
Development

El shell del editor de posts, un bug de token vencido, y sin gema de mocking

Por Victor Da Luz
railsrubytestingdev-logblog-manager

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:

  1. Mi primera versión de la acción edit siempre construía un Github::ContentClient antes de revisar si ya existía un borrador, lo que significaba que reabrir un borrador ya abierto explotaba con AuthError en 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.

  2. Minitest 6 separó minitest/mock del núcleo sin hacer mucho ruido, Object#stub no 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 con define_singleton_method/remove_method en 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