Visão geral
API completa de votação eletrônica: criação de eleição, opções de voto, abertura, votação,
encerramento e apuração, cada etapa numa máquina de estados que só anda pra frente
(Rascunho → Aberta → Encerrada → Apurada). Enquanto a eleição está aberta, só o total de
votos é público (via SignalR), nunca a contagem por opção, pra não influenciar quem ainda
não votou.
Anonimato do voto sem abrir brecha pro voto duplicado
O voto em si (Voto) não guarda nenhuma referência a quem votou. Quem impede voto duplicado
é uma entidade completamente separada, ComprovanteDeParticipacao, que sabe só "este eleitor
já votou nesta eleição", identificado por um hash HMAC-SHA256 do id dele (nunca o id em
texto puro). As duas entidades não se referenciam. É essa separação que garante o sigilo,
não uma regra de acesso.
Cadeia de auditoria
Cada voto grava o hash SHA-256 de si mesmo encadeado ao hash do voto anterior da mesma eleição, o mesmo princípio de blockchain aplicado sem o resto do aparato (sem consenso distribuído, é um banco relacional comum). Alterar um voto já gravado quebra a cadeia a partir dali, e a apuração recusa fechar se a cadeia não bater.
Dois bugs reais pegos pelos testes
O hash originalmente usava DateTime.ToString("O"), que muda de string dependendo do
DateTimeKind. O SQLite não preserva Kind, um voto salvo como Utc voltava do banco como
Unspecified, e isso derrubava a verificação da cadeia sem nenhuma adulteração real ter
acontecido. Corrigido usando Ticks no lugar do formato de data.
O retry de concorrência (quando dois votos disputam o mesmo elo da cadeia) reconstruía voto
e comprovante a cada tentativa sem limpar o rastreamento do DbContext da tentativa anterior
que tinha falhado, duplicando o comprovante do mesmo eleitor. Só apareceu com um teste de
verdade disparando 15 votos simultâneos via HTTP. Corrigido limpando o ChangeTracker antes
de cada nova tentativa.
Arquitetura
Clean Architecture em quatro camadas (Domain, Application, Infrastructure, Api), EF Core + SQLite, SignalR pro total ao vivo, middleware central traduzindo exceção de domínio em status HTTP.
Testes
44 testes: Domain (máquina de estados, cadeia de auditoria, a regressão do DateTimeKind),
Application (SQLite real, não o provider InMemory, incluindo concorrência de verdade com
Task.WhenAll) e Integration (fluxo HTTP completo de ponta a ponta via
WebApplicationFactory, incluindo 15 votos simultâneos).