A casca do editor de posts, um bug de token obsoleto e nenhuma gem de mocking
Me propus a construir a casca do novo editor de posts do blog-manager, /posts/:id/edit, uma tabela de rascunhos com persistência no banco, autosave e uma ação de descartar. É a primeira de um pequeno conjunto de issues do editor (preview, serializer de frontmatter, tags, tradução, painel de imagem de destaque, fluxo de commit) que estavam todas paradas no backlog sem ordem de dependência entre elas, então antes de tocar no código eu precisei descobrir qual delas estava realmente desbloqueada.
O que eu construí
Uma tabela editor_drafts (post_id, locale, frontmatter, body, base_file_sha) e um model EditorDraft. O frontmatter é um Hash serializado em JSON, seguindo o mesmo padrão serialize ..., coder: JSON que a coluna Post#tags já usa. O SQLite ainda não tem nenhum precedente de t.json nesse app, e eu não queria ser quem introduz uma nova convenção de tipo de coluna numa issue fundacional.
PostEditor::DraftLoader, um serviço pequeno que ou retorna um rascunho existente ou constrói um buscando o arquivo ao vivo no GitHub e separando em frontmatter/body. Ele copia quase ao pé da letra o fallback de path en/-depois-plano do committer de imagem de destaque; o padrão já estava ali, sem motivo para inventar um novo.
Além disso: PostsController#edit/#update_draft/#discard_draft, uma view com <textarea> simples com painéis de placeholder para o que as próximas issues vão preencher, e um controller Stimulus para o autosave (com debounce de ~1,5s, Tab insere dois espaços em vez de mover o foco, Cmd/Ctrl-S salva imediatamente).
Decisões e por quê
GET /posts/:id/edit é a primeira ação de controller nesse app que chama o GitHub de forma síncrona. Toda chamada existente ao GitHub (scanning, commits de imagem de destaque) roda dentro de um background job. Fiquei em dúvida sobre isso, mas ler um único arquivo no carregamento da página é pequeno e rápido o suficiente para que uma UI de job mais polling fosse mais maquinário do que o problema exige; jobs se justificam para escritas (que precisam de retries e estado de erro) e leituras em lote (scanning), não para um GET único. Precisei criar uma convenção de rescue para Github::ContentClient::Error no nível do controller, já que não existia nada parecido para copiar.
O que me surpreendeu
Duas coisas que só peguei porque fui de fato usar a página num navegador em vez de confiar nos testes verdes:
-
Minha primeira versão da ação
editsempre construía umGithub::ContentClientantes de checar se já existia um rascunho, o que significava que reabrir um rascunho já aberto explodia comAuthErrorno momento em que o token de um blog ficasse obsoleto ou ausente, mesmo que nada naquela requisição precisasse do GitHub. Correção fácil assim que vi (checar primeiro se existe um rascunho, só construir o client se houver de fato uma busca a fazer), mas é exatamente o tipo de bug que uma suíte de testes com tudo mockado deixa passar direto. -
O Minitest 6 silenciosamente separou o
minitest/mockdo core.Object#stubnão está disponível a menos que você exija a gem separada, e ela nem vem empacotada nessa configuração de Ruby. Como esse app também não tem Mocha nem WebMock, fiz à mão um stub helper comdefine_singleton_method/remove_methodem vez de buscar uma nova dependência.
Eu também não tinha um script de verificação pronto para recorrer, então checar de ponta a ponta significou escrever à mão uma sessão real de login mais editor com Playwright (esse app não tem package.json nenhum, é importmap-rails, sem etapa de build Node) e rodar um scan do axe-core na página nova. Esse scan pegou uma falha real: a página do editor não tinha nenhum <h1>, algo que a página de show ganha de graça pelo heading do título do post, mas meu layout novo não tinha.
Antes de tudo isso eu tinha passado pelas outras cinco issues do editor e descobri que todas haviam sido criadas com dependências umas nas outras que não estavam refletidas no estado delas. A issue de fluxo de commit, em particular, dizia se apoiar numa tabela de rascunhos e num serializer de frontmatter que ainda não existiam. Adicionei um estado Blocked ao projeto, movi tudo que estava de fato bloqueado para lá com um comentário nomeando o bloqueio, e deixei essa como o único item desbloqueado, o que tornou fácil decidir “começar por aqui”.
O que vem a seguir
O serializer de frontmatter e o resto do conjunto de issues do editor são os próximos da fila, agora que a casca e a tabela de rascunhos das quais dependem já existem.
Leitura relacionada
Escrevendo testes que checam a coisa certa
Duas lacunas de cobertura fechadas, e a pergunta mais útil que uma revisão fez depois: não «isso passa» mas «o que esse teste realmente pegaria».
O que acontece quando um job faz broadcast para ninguém
Fechando o loop da imagem de destaque: frontmatter só por inserção, uma guarda que pegou divergência real na primeira execução, e um broadcast sem ouvinte.
Uma correção de desvio de documentação que não foi tão chata quanto parecia
Três itens da auditoria que cada um virou outra coisa: uma alegação meio corrigida, uma recuperação de senha silenciosamente morta, e um e-mail de staging que apontaria para produção.
Você também pode achar útil
NordPass
Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.
Como afiliado da NordPass, ganho com compras qualificadas.
Saiba maisProton Drive
Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maiseSIM Airalo
eSIM de dados local para viagens - sem necessidade de trocar um SIM físico.
Este é meu link de indicação da Airalo. Você recebe um desconto no seu primeiro eSIM e eu ganho crédito da Airalo para o meu.
Saiba mais