# ENQUETES — Estratégia de Testes

> **Versão analisada:** `v6`
> Framework legado: PHPUnit `~6.0` (`v6/composer.json`), `v6/phpunit.xml`.
> Execução: `vendor/bin/phpunit` a partir de `v6/`.

---

## 1. Situação atual

**O módulo Enquetes não possui nenhum teste.** A suíte existente cobre
apenas o Dashboard (`v6/tests/Feature/Dashboard*.php`,
`v6/tests/Unit/Dashboard*.php`) e alguns utilitários
(`v6/tests/Feature/GaConfigurationTest.php`,
`v6/tests/Feature/GaWidgetsTest.php`,
`v6/tests/Unit/GoogleAnalyticsServiceTest.php`).

| Item | Situação |
|---|---|
| Testes do módulo Enquetes | **0** |
| Factories de `Survey`, `SurveyAnswer`, `SurveyResult` | **inexistentes** (`v6/database/factories/` tem apenas `HighlightFactory.php`, `ManualAreaFactory.php`, `ManualFactory.php`, `UserFactory.php`) |
| TestCase base do módulo | inexistente; o padrão a seguir é `v6/tests/DashboardTestCase.php` |
| Banco de teste | o `phpunit.xml` **não** define `DB_CONNECTION`; o `DashboardTestCase` usa `DatabaseTransactions` (rollback por teste) sobre o banco configurado em `.env` |

### 1.1 Convenções a seguir (herdadas de `v6/tests/DashboardTestCase.php`)

- `protected function setUp()` (sem `void` — PHPUnit 6).
- `use DatabaseTransactions` para não sujar o banco.
- `require_once` de `app/Library/Helper/appHelpers.php`, porque os helpers
  globais `has_access()` / `is_admin()` não são autoloadados pelo Composer.
- Supressão de `E_NOTICE` no bootstrap (o `config/app.php` legado gera notice).
- Criação de papéis e usuários Sentinel direto no banco
  (`createRole()`, `createUser()` do `DashboardTestCase`).

## 2. Estratégia

| Camada | Foco | Ferramenta |
|---|---|---|
| **Feature (CMS)** | Criação, edição, validação, exclusão, exclusão em lote, permissões | `$this->post/put/delete(...)` + `assertStatus`, `assertSessionHas`, `assertDatabaseHas` |
| **Feature (Portal)** | Listagem, redirecionamento por vencimento, gravação do voto, apuração | `$this->get(...)`, `$this->postJson(...)` |
| **Unit** | Cálculo de percentual, regra de "já votou", geração de slug, escopo de enquete ativa | PHPUnit puro sobre `SurveyTallyService`/`Survey` |
| **Regressão de paridade** | Percentuais do legado vs. v7 | Script de comparação sobre `survey_results` |

### 2.1 Fixtures necessárias

```php
// (ilustrativo) database/factories/SurveyFactory.php
Survey::factory()
    ->published()
    ->closingAt(now()->addDays(7))
    ->withAnswers(['Sim', 'Não']);
```

Factories a criar:

| Factory | Campos |
|---|---|
| `SurveyFactory` | `question`, `slug` (único), `status`, `publication_date`, `final_date`, `state` (`draft` / `published` / `expired`) |
| `SurveyAnswerFactory` | `survey_id`, `answer` |
| `SurveyResultFactory` | `survey_id`, `survey_answer_id`, `ip`, `browser` |

> O módulo **não** tem seeds; os factories substituem a necessidade de seed de
> teste. A view SQL `enquetes` precisa existir no banco de teste — ela é criada
> pelas migrations `2023_04_06_134445` e `2023_05_25_093023`, portanto o
> `migrate` do banco de teste já a cria.

## 3. Como rodar

```bash
cd v6

# Suíte completa
vendor/bin/phpunit

# Apenas o módulo Enquetes (após criar os arquivos em tests/)
vendor/bin/phpunit --filter Survey

# Arquivo específico
vendor/bin/phpunit tests/Feature/Portal/SurveyVoteTest.php
```

Variáveis de ambiente relevantes (do `.env` do ambiente de teste):

| Variável | Valor esperado |
|---|---|
| `APP_ENV` | `testing` (definido em `v6/phpunit.xml`) |
| `DB_CONNECTION` | PostgreSQL do ambiente de teste |
| `CACHE_DRIVER` | `array` (definido em `v6/phpunit.xml`) |
| `SESSION_DRIVER` | `array` |
| `QUEUE_DRIVER` | `sync` |

## 4. Casos de teste concretos

> **Como ler as tabelas abaixo:** os casos descrevem o **comportamento
> desejado na v7**. Quando o texto diz *"comportamento legado"*, isso indica
> que o código atual (`v6`) se comporta de outra forma — o teste existe para
> **documentar a regressão** e falhar até a correção. Os identificadores
> `A1`..`A30` citados entre parênteses remetem à lista de armadilhas em
> `05-REGRAS-E-PORTABILIDADE.md` §3.1.

### 4.1 Feature — CMS (criação e edição)

| # | Nome | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | `testStoreCreatesSurveyAndAnswers` | Usuário `super-admin` autenticado | `POST /cms/enquete` com `question`, `publication_date`, `final_date`, `answers[0][answer]`, `answers[1][answer]`, `status=on` | `assertStatus(302)`, `assertRedirect('/cms/enquete')`, `assertDatabaseHas('surveys', ['question' => ...])`, `assertDatabaseCount('survey_answers', 2)` |
| C2 | `testStoreGeneratesSlugFromQuestion` | idem | `POST` com `question = "Prefere o novo hospital?"` | `assertDatabaseHas('surveys', ['slug' => 'prefere-o-novo-hospital'])` |
| C3 | `testStoreAppendsSuffixOnSlugCollision` | Já existe `surveys.slug = "enquete-teste"` | `POST` com `question = "Enquete teste"` | `assertDatabaseHas('surveys', ['slug' => 'enquete-teste-1'])` |
| C4 | `testStoreRequiresQuestion` | idem | `POST` sem `question` | `assertSessionHasErrors('question')`, `assertDatabaseCount('surveys', 0)` |
| C5 | `testStoreRequiresAnswers` | idem | `POST` sem `answers` | `assertSessionHasErrors('answers')` |
| C6 | `testStoreConvertsBrazilianDateFormat` | idem | `POST` com `publication_date = "01/02/2026 08:00"` | `assertDatabaseHas('surveys', ['publication_date' => '2026-02-01 08:00:00'])` |
| C7 | `testUnpublishOnUpdateKeepsStatusTrue` | Enquete publicada com 1 voto | `PUT /cms/enquete/{id}` **sem** `status` | Comportamento legado: `status` continua `true` → o teste **deve falhar** na v7 (regressão a corrigir, ver A3 em `05-REGRAS-E-PORTABILIDADE.md`) |
| C8 | `testUpdateDestroysAndRecreatesAnswers` | Enquete com 2 opções e 1 voto | `PUT /cms/enquete/{id}` com 3 opções | `assertDatabaseCount('survey_answers', 3)` (soft-deleted ainda no banco) e **os ids das opções são novos** (A8) |
| C9 | `testEditDisablesFormWhenSurveyHasResults` | Enquete com 1 `survey_results` | `GET /cms/enquete/{id}/edit` | A resposta contém "Essa enquete não pode ser editada" |
| C10 | `testDestroyRemovesSurveyAndAnswers` | Enquete com 2 opções | `DELETE /cms/enquete/{id}` | `assertStatus(200)`, `assertDatabaseHas('surveys', ['id' => $id, 'deleted_at' => ...])`, opções com `deleted_at` preenchido |
| C11 | `testBulkDeleteRemovesSurveys` | 3 enquetes | `POST /cms/ajax/enquete/delete-por-lote` com `toDelete[]` | `assertStatus(200)`, `assertJson(['success' => 'Enquetes deletadas com sucesso.'])`, 3 soft-deletes |
| C12 | `testDatatableReturnsEnquetesView` | 2 enquetes, 1 com 3 votos | `GET /cms/datatable/enquete` | `assertStatus(200)`, JSON contém `pergunta`, `qtd_respostas = 3`, `data_publicacao` |

### 4.2 Feature — CMS (autorização)

| # | Nome | Arrange | Act | Assert |
|---|---|---|---|---|
| A1 | `testAnonymousIsRedirectedFromIndex` | sem sessão | `GET /cms/enquete` | `assertStatus(302)` (middleware `check.logged`) |
| A2 | `testBulkDeleteRequiresAuthentication` | sem sessão, mas com token CSRF válido | `POST /cms/ajax/enquete/delete-por-lote` | **Comportamento legado: retorna 200 e exclui.** Na v7 deve retornar 401/403 (A20 em `05-REGRAS-E-PORTABILIDADE.md`) |
| A3 | `testDeleteAnswerRouteIsDead` | — | `POST /cms/ajax/ajax-delete-answer` | **Comportamento legado: 500** (`BadMethodCallException`). Na v7 a rota deve existir ou ser removida (A21) |

### 4.3 Feature — Portal

| # | Nome | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | `testIndexListsOnlyPublishedSurveys` | 1 publicada, 1 rascunho, 1 com `publication_date` futura | `GET /enquetes` | A resposta contém a pergunta da publicada e **não** contém as outras duas |
| P2 | `testShowRedirectsToClosedWhenExpired` | Enquete com `final_date` no passado | `GET /enquetes/{slug}` | `assertRedirect('enquetes/encerrada/'.$slug)` |
| P3 | `testShowRendersVotingForm` | Enquete ativa com 2 opções | `GET /enquetes/{slug}` | Contém `survey_answer_id` × 2 e o botão "Votar" |
| P4 | `testShowReturns500ForUnknownSlug` | — | `GET /enquetes/nao-existe` | **Comportamento legado: 500.** Na v7 deve ser 404 (A9) |
| P5 | `testVoteStoresResult` | Enquete ativa com 2 opções | `POST /send-survey-ajax` com `survey_id` e `survey_answer_id` | `assertStatus(200)`, JSON `[0] === 201`, `assertDatabaseHas('survey_results', ['survey_id' => ..., 'survey_answer_id' => ...])`, `ip` e `browser` preenchidos |
| P6 | `testVoteWithoutAnswerIsRejected` | — | `POST /send-survey-ajax` sem `survey_answer_id` | JSON `[0] === 400` e mensagem "Por favor, marque uma das opções"; `assertDatabaseCount('survey_results', 0)` |
| P7 | `testVoteIsAcceptedOnExpiredSurvey` | Enquete vencida | `POST /send-survey-ajax` válido | **Comportamento legado: 201 e voto gravado.** Na v7 deve ser 422 (A4) |
| P8 | `testVoteAcceptsAnswerFromAnotherSurvey` | Duas enquetes | `POST` com `survey_id` de A e `survey_answer_id` de B | **Comportamento legado: 201 e voto gravado.** Na v7 deve ser 422 (A5) |
| P9 | `testDuplicateVoteIsAccepted` | Enquete ativa | Duas vezes `POST /send-survey-ajax` | **Comportamento legado: 2 registros.** Na v7, com chave única, apenas 1 (A6) |
| P10 | `testClosedPageShowsPercentage` | 4 votos: 3 na opção A, 1 na opção B | `GET /enquetes/encerrada/{slug}` | Contém "75.00 %" e "25.00 %" |
| P11 | `testClosedPageWithNoVotesShowsZero` | Enquete encerrada sem votos | `GET /enquetes/encerrada/{slug}` | Contém "0.00 %" e **não** gera erro de divisão por zero (R7) |
| P12 | `testClosedRedirectsBackToVotingWhenStillOpen` | Enquete ativa | `GET /enquetes/encerrada/{slug}` | `assertRedirect('enquetes/'.$slug)` |
| P13 | `testSidebarShowsLatestActiveSurvey` | 2 enquetes ativas | `GET /enquetes` | A sidebar (`#enquetes_sidebar`) mostra apenas a mais recente |
| P14 | `testSidebarHiddenWhenModuleDisabled` | `modulos.status` de `enquetes` = `false` | `GET /enquetes` | Não contém `#enquetes_sidebar` |

### 4.4 Unit

| # | Nome | Arrange | Act | Assert |
|---|---|---|---|---|
| U1 | `testPercentageCalculation` | — | `calcularPorcentagem(3, 4)` | `75.0` |
| U2 | `testPercentageOfZeroOverZeroIsZero` | — | `calcularPorcentagem(0, 0)` | `0` (sem divisão por zero) |
| U3 | `testSlugIsDerivedAndTruncatedTo50` | — | `slugify(str_limit($q, 50))` | Slug com no máximo 50 caracteres |
| U4 | `testAlreadyVotedComparesAllResultsOfSurvey` | 2 votos: IP A e IP B | `survey->wasVotedBy('A.1.1.1')` | `true` (o legado só olhava o primeiro registro e retornaria `false`) |
| U5 | `testScopeActiveFiltersPublishedAndNotExpired` | 4 enquetes em estados distintos | `Survey::active()->get()` | Apenas a que está publicada e não vencida |
| U6 | `testSurveyIsOpenRespectsBothDates` | 4 combinações de `status`/`publication_date`/`final_date` | `isOpen()` | `true` somente na combinação válida |

### 4.5 Regressão de paridade (apuração)

| # | Nome | Arrange | Act | Assert |
|---|---|---|---|---|
| R1 | `testLegacyPercentagesMatchV7` | Enquete do banco legado com N votos | Compara o percentual do legado (`count/total*100`, `number_format(...,2)`) com o da v7 | Igualdade exata para todas as opções, inclusive quando o total é 0 |

## 5. Cenários manuais de verificação (sem automação)

| # | Cenário | Resultado esperado no legado |
|---|---|---|
| M1 | CMS: criar enquete com 2 opções, publicar, expirar em 1 hora | Salva; **não** exibe "Cadastrado com sucesso" (A1) |
| M2 | Portal: `/enquetes` | Lista a enquete; sidebar mostra a enquete ativa |
| M3 | Portal: votar | Formulário substituído por "Voto salvo com sucesso!" |
| M4 | Portal: recarregar `/enquetes/{slug}` com o mesmo IP | Mostra "Você já votou!" (se o IP for igual ao do **primeiro** voto) |
| M5 | CMS: editar enquete **sem** votos | Funciona; a URL pública muda se o texto da pergunta mudou (R11) |
| M6 | CMS: editar enquete **com** votos | Formulário desabilitado com a mensagem "Essa enquete não pode ser editada" |
| M7 | CMS: desmarcar "Publicar" e salvar | O status **não** muda (A3) |
| M8 | CMS: excluir em lote pela listagem | Confirmação SweetAlert; requisição `POST` com `toDelete[]`; lista recarrega |
| M9 | Portal: `/enquetes/encerrada/{slug}` | Percentuais com barra de progresso; a data exibida é a de **publicação** (A10) |
| M10 | Portal: ativar o layout `default` como `configurations.layout` | HTTP 404 em todas as telas de enquete |

## 6. Não testável / fora do escopo automatizado

| Item | Motivo |
|---|---|
| Exportação Excel/PDF do DataTable | O botão é do plugin DataTables (client-side); verificar visualmente no navegador |
| `addthis_inline_share_toolbox` | Serviço externo de terceiros |
| Precisão do `@datetimepicker` (AdminLTE) | Componente de front-end |
| Estilo visual das 6 sidebars | Validado por inspeção visual ou snapshot de view |
| Integração com `mailhog` | O módulo não envia e-mail |

## 7. Critérios de aceite do dossiê

Ao entregar a suíte do módulo Enquetes na v7:

1. Todos os casos C1..C12, A1..A3, P1..P14 e U1..U6 existem e passam.
2. Os casos marcados como **"comportamento legado"** (C7, A2, A3, P4, P7, P8, P9)
   devem ter sido **corrigidos**; se algum comportamento for mantido
   deliberadamente, o teste deve documentar a decisão no comentário.
3. O teste de paridade R1 roda contra um dump anonimizado de `survey_results`.
4. Nenhum teste escreve fora do schema de teste (usar `DatabaseTransactions`).
