Skip to content

ramcodev/simplevoting

Repository files navigation

Simple Voting

Sistema de votação desenvolvido como teste técnico para a NTT Data. Permite que administradores cadastrem enquetes com opções de resposta e que usuários autenticados votem — tanto pela interface do próprio Drupal quanto por uma API REST construída manualmente.

Requisitos técnicos atendidos: entidades construídas sem node, API implementada sem JSON:API, ambiente via Lando, dump de banco incluído, collection Postman disponível.


Requisitos

Dependência Versão Observação
Lando v3.x Orquestra os containers Docker
Docker Engine 24.x+ Backend do Lando
Git qualquer

Não é necessário PHP, Composer ou Drush instalados globalmente — tudo roda dentro dos containers gerenciados pelo Lando.


Instalação e configuração

1. Clone o repositório

git clone https://github.com/bielcode/simplevoting.git
cd simplevoting

2. Instale o Docker (se necessário)

curl -fsSL https://get.docker.com | sudo bash
sudo usermod -aG docker $USER
# Faça logout e login novamente para o grupo docker ser reconhecido

3. Instale o Lando

/bin/bash -c "$(curl -fsSL https://get.lando.dev/setup-lando.sh)" -- --yes
echo 'export PATH="$HOME/.lando/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

4. Suba o ambiente

lando start

Na primeira execução o Lando baixa as imagens Docker (PHP 8.2, Apache 2.4, MariaDB 10.6) e cria automaticamente:

  • .env — a partir de .env.example (credenciais e configurações do ambiente)
  • web/sites/default/settings.local.php — a partir de example.settings.local.php

Nenhum arquivo precisa ser criado manualmente.

5. Instale as dependências PHP

lando composer install

6. Restaure o banco de dados

O repositório inclui o dump completo em dump/simplevoting.sql, com enquetes e votos de exemplo prontos para demonstração.

lando db-import dump/simplevoting.sql
lando drush cr

Login padrão: admin / admin

Instalação do zero (sem dump)
lando drush site:install standard \
  --db-url=mysql://drupal10:drupal10@database/drupal10 \
  --account-name=admin \
  --account-pass=admin \
  --account-mail=admin@example.com \
  --site-name="Simple Voting" \
  -y

lando drush en simple_voting -y
lando drush cr

7. Acesse o site

https://simplevoting.lndo.site

O certificado é autoassinado — aceite a exceção de segurança no browser na primeira vez.


Interface administrativa

Acesse /admin/config/simple-voting/questions (menu: Configurações → Simple Voting → Voting Questions).

Gerenciamento de enquetes:

  • Criar, editar e excluir enquetes via formulário intuitivo
  • Cada enquete tem um identificador único (machine name) imutável após criação
  • Opções de resposta com título (obrigatório), descrição e imagem — quantidade ilimitada, adicionadas via AJAX sem recarregar a página
  • Por enquete: configurar se o total de votos é exibido ou ocultado após a votação
  • Status individual: aberta ou encerrada

Configurações globais (/admin/config/simple-voting/settings):

  • Habilitar ou desabilitar a votação de forma geral — quando desabilitado, nenhuma enquete aceita votos, independente do status individual, e o bloqueio se aplica tanto à interface CMS quanto à API

Permissões (/admin/people/permissions):

Permissão Descrição
administer simple voting Criar/editar enquetes e acessar configurações
vote in polls Registrar votos (concedida ao authenticated user na instalação)
view voting results Visualizar resultados mesmo quando ocultos na enquete

Interface de votação (CMS)

Acesse /voting para ver a lista de enquetes disponíveis.

  • Cada enquete tem URL própria: /voting/{id}
  • O usuário seleciona uma opção e clica em "Registrar voto"
  • Um usuário não pode votar mais de uma vez na mesma enquete — tentativas duplicadas exibem aviso sem gerar erro
  • Após votar, o usuário é redirecionado para a página de resultados (/simple-voting/results/{id})
  • Os resultados são exibidos ou ocultados conforme a configuração individual de cada enquete
  • O formulário também pode ser incorporado em qualquer região do site via bloco "Simple Voting: Formulário de Votação"

API REST

Base URL: https://simplevoting.lndo.site

Todos os endpoints exigem autenticação de usuário Drupal. Use HTTP Basic Auth em chamadas externas (Postman, aplicações mobile etc.). O endpoint de voto exige adicionalmente o header X-CSRF-Token para clientes de sessão — obtido via GET /session/token.

Endpoints

GET /api/voting/v1/questions

Lista todas as enquetes com status aberto.

curl -u admin:admin https://simplevoting.lndo.site/api/voting/v1/questions
{
  "data": [
    { "id": "enquete-exemplo", "title": "Qual sua linguagem favorita?", "show_results": true }
  ]
}

GET /api/voting/v1/questions/{id}

Retorna os detalhes de uma enquete com suas opções de resposta.

curl -u admin:admin https://simplevoting.lndo.site/api/voting/v1/questions/enquete-exemplo
{
  "data": {
    "id": "enquete-exemplo",
    "title": "Qual sua linguagem favorita?",
    "status": "open",
    "show_results": true,
    "options": [
      { "id": 1, "title": "PHP", "description": "A linguagem da web" },
      { "id": 2, "title": "Python" }
    ]
  }
}

POST /api/voting/v1/votes

Registra um voto. Um usuário pode votar apenas uma vez por enquete.

curl -u admin:admin \
  -H "Content-Type: application/json" \
  -d '{"question_id": "enquete-exemplo", "option_id": 1}' \
  https://simplevoting.lndo.site/api/voting/v1/votes
{ "message": "Voto registrado com sucesso." }

Códigos de resposta relevantes:

Código Situação
201 Voto registrado
400 Body inválido ou campos ausentes
401 Sem autenticação
409 Usuário já votou nesta enquete
422 Enquete encontrada, mas fechada para votos
503 Votação globalmente desabilitada ou sobrecarga momentânea

GET /api/voting/v1/questions/{id}/results

Retorna a contagem de votos por opção com percentual. Respeita a configuração show_results da enquete — se estiver desabilitada, apenas usuários com a permissão view voting results recebem os dados; demais recebem 403.

curl -u admin:admin https://simplevoting.lndo.site/api/voting/v1/questions/enquete-exemplo/results
{
  "data": {
    "question_id": "enquete-exemplo",
    "question_title": "Qual sua linguagem favorita?",
    "total_votes": 3,
    "options": [
      { "id": 1, "title": "PHP", "votes": 2, "percentage": 66.7 },
      { "id": 2, "title": "Python", "votes": 1, "percentage": 33.3 }
    ]
  }
}

Collection Postman

Importe o arquivo postman/simple_voting.postman_collection.json no Postman. A collection já inclui todos os endpoints com exemplos de request/response e variável base_url configurável para o ambiente local ou qualquer outro.


Testes

Os testes unitários cobrem a lógica central de registro de votos, focando nos cenários de concorrência e proteção de duplicidade que não são viáveis de reproduzir manualmente.

lando test-unit
Voting Service (Drupal\Tests\simple_voting\Unit\Service\VotingService)
 ✔ Registra um voto novo com os campos corretos quando não há duplicata
 ✔ Lança DuplicateVoteException quando o SELECT detecta voto anterior (camada 1)
 ✔ Trata violação de unique constraint como voto duplicado, não como erro fatal (camada 2)
 ✔ Lança VoteLockUnavailableException sem tocar no banco quando o lock falha duas vezes
 ✔ Prossegue normalmente quando o lock é adquirido na segunda tentativa
 ✔ Libera o lock mesmo quando o INSERT lança uma exceção inesperada (finally garantido)
 ✔ Persiste IP vazio sem erro quando executado fora de contexto HTTP (CLI/Drush)

OK (7 tests, 18 assertions)

Comandos úteis

lando start                    # sobe o ambiente
lando stop                     # para o ambiente
lando drush cr                 # limpa o cache do Drupal
lando drush uli                # gera link de login de um clique (sem precisar da senha)
lando drush updb -y            # aplica atualizações de schema pendentes
lando drush sql:dump --result-file=/app/dump/simplevoting.sql  # gera dump
lando composer install         # instala dependências PHP
lando test-unit                # roda os testes unitários do módulo
lando phpcs                    # audita o código contra o padrão Drupal/DrupalPractice
lando phpcbf                   # aplica correções de coding standard automaticamente
lando ssh                      # acesso ao terminal do container

phpMyAdmin disponível em http://localhost:32770 (confirme a porta com lando info).


Estrutura do módulo

web/modules/custom/simple_voting/
├── config/
│   ├── install/
│   │   └── simple_voting.settings.yml   # configuração padrão (votação habilitada)
│   └── schema/
│       └── simple_voting.schema.yml     # schema de validação da config
├── src/
│   ├── Breadcrumb/
│   │   └── VotingBreadcrumbBuilder.php  # hierarquia Home > Enquetes > Questão > Resultado
│   ├── Controller/
│   │   ├── VotingApiController.php      # endpoints da API REST v1
│   │   ├── VotingPageController.php     # páginas públicas de listagem e votação
│   │   └── VotingResultsController.php  # página de resultados pós-voto
│   ├── Entity/
│   │   ├── VotingQuestion.php           # ConfigEntity (sem node)
│   │   ├── VotingQuestionInterface.php  # contrato da entidade
│   │   └── VotingQuestionListBuilder.php
│   ├── EventSubscriber/
│   │   └── VotingAccessDeniedSubscriber.php  # redireciona anônimos ao login
│   ├── Exception/
│   │   ├── DuplicateVoteException.php
│   │   └── VoteLockUnavailableException.php
│   ├── Form/
│   │   ├── QuestionForm.php             # CRUD de enquetes + opções via AJAX
│   │   ├── VoteForm.php                 # formulário de votação do usuário
│   │   └── VotingSettingsForm.php       # habilitar/desabilitar votação global
│   ├── Plugin/Block/
│   │   └── VotingBlock.php              # bloco configurável para regiões do tema
│   └── Services/
│       └── VotingService.php            # lógica de voto com lock + unique constraint
├── tests/
│   └── src/Unit/Service/
│       └── VotingServiceTest.php        # 7 testes unitários com mocks
├── phpunit.xml
├── simple_voting.info.yml
├── simple_voting.install                # hook_schema, hook_install, update hooks
├── simple_voting.links.action.yml
├── simple_voting.links.menu.yml
├── simple_voting.module
├── simple_voting.permissions.yml
├── simple_voting.routing.yml

Concorrência e integridade dos dados

O requisito de unicidade do voto por usuário precisa ser garantido mesmo quando múltiplas requisições chegam simultaneamente para o mesmo par (uid, question_id) — situação realista em duplo clique, retry automático do cliente ou deploys com múltiplos workers PHP.

A proteção é implementada em duas camadas independentes e complementares dentro de VotingService::castVote():

Camada 1 — Lock de aplicação (LockBackendInterface)

Antes de qualquer leitura ou escrita no banco, o serviço tenta adquirir um lock nomeado simple_voting_vote_{uid}_{question_id}. O primeiro processo a adquirir o lock executa o SELECT de verificação de duplicidade e o INSERT de forma logicamente atômica. Qualquer processo concorrente para o mesmo par fica bloqueado em lock->wait() e, ao receber o lock, já encontra o voto persistido.

Sem essa camada, dois processos poderiam executar o SELECT simultaneamente, ambos retornar "não votou ainda" e ambos prosseguir para o INSERT — deixando a decisão de qual commit vence para o banco, de forma imprevisível.

Se o lock não puder ser adquirido em duas tentativas (carga extrema ou lock travado), VoteLockUnavailableException é lançada e o voto é rejeitado com HTTP 503.

Camada 2 — Unique constraint no banco (question_id, uid)

Garante integridade nos cenários que o lock de aplicação não cobre:

  • Múltiplos servidores web sem backend de lock compartilhado (memcache/Redis) — o lock local não é visível entre máquinas.
  • Falha de infraestrutura que derruba o processo antes de ele liberar o lock, permitindo que outro servidor adquira um novo lock enquanto o INSERT do processo anterior ainda está em andamento.
  • Qualquer outro caminho de código que insira votos sem passar pelo serviço.

IntegrityConstraintViolationException é capturada e tratada como DuplicateVoteException — uma duplicata normal, não um erro fatal.

O lock é sempre liberado no bloco finally, garantindo que a liberação ocorra mesmo quando o INSERT lança exceção inesperada.


Observabilidade

O módulo registra eventos relevantes em um canal de log dedicado (simple_voting), separado do canal default do Drupal para facilitar filtragem e monitoramento.

Canal: logger.channel.simple_voting

Eventos registrados:

Nível Situação Local
warning Lock indisponível após duas tentativas — possível lock travado ou carga anormal VotingService
notice Voto duplicado detectado via SELECT dentro do lock VotingService
notice Unique constraint violada — indício de lock backend não compartilhado entre servidores VotingService
error Falha inesperada ao persistir voto (inclui stack trace) VotingService
warning Enquete não encontrada em requisição à API VotingApiController
notice Voto duplicado recebido via API VotingApiController
warning Lock indisponível em requisição à API VotingApiController
error Falha inesperada em qualquer endpoint da API VotingApiController

Como acessar:

# Interface web — filtra pelo canal do módulo
# Acesse: /admin/reports/dblog?type[]=simple_voting

# Via Drush — últimas 50 entradas do canal
lando drush watchdog:show --type=simple_voting --count=50

# Todos os erros e warnings do módulo
lando drush watchdog:show --type=simple_voting --severity=Warning --count=100

Em produção com múltiplos servidores, configure o Drupal para usar um syslog ou agregador externo (Loki, Datadog, CloudWatch). Eventos de warning recorrentes com a mensagem de unique constraint indicam ausência de lock backend compartilhado (memcache ou Redis) entre os nós.

About

Teste de Sistema de Votação Simples

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors