Pular para o conteúdo
Development

A casca do editor de posts, um bug de token obsoleto e nenhuma gem de mocking

Por Victor Da Luz•
railsrubytestingdev-logblog-manager

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:

  1. Minha primeira versão da ação edit sempre construía um Github::ContentClient antes de checar se já existia um rascunho, o que significava que reabrir um rascunho já aberto explodia com AuthError no 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.

  2. O Minitest 6 silenciosamente separou o minitest/mock do core. Object#stub nã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 com define_singleton_method/remove_method em 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

Você também pode achar útil

NordPass

NordPass

Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.

Como afiliado da NordPass, ganho com compras qualificadas.

Saiba mais
Proton

Proton 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 mais
Airalo

eSIM 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