# Relatório Estatístico da Ouvidoria — Testes

> A v5 **não possui nenhum teste** para este módulo: `v5/tests/Feature/` contém apenas
> `ExampleTest.php` e `v5/tests/Unit/` apenas `CnpjAlphanumericValidationTest.php` e
> `ExampleTest.php`. O que segue é o plano de testes para a reconstrução (v7) e também
> serve como roteiro de validação manual do legado.

---

## 1. Estratégia

| Tipo | Escopo | Ferramenta (v7) |
|---|---|---|
| **Feature (HTTP)** | Rotas do CMS (autenticação, permissões, CRUD, upload) e do portal (página, filtro, exportações, Ajax) | PHPUnit 11 + `RefreshDatabase` |
| **Unit** | Service de agregação (prazos, distribuições, normalização de gênero) | PHPUnit 11, sem banco |
| **Integração (SQL)** | Semântica de `EXTRACT(DAY FROM ...)` e limites de 20/21 dias | Teste de feature com banco real (PostgreSQL) |
| **Manual** | Renderização dos gráficos (Google Charts), Appearance nos 6 templates do portal | Checklist manual |

> O banco de teste **deve** ser PostgreSQL (o SQL usa `EXTRACT`, `whereYear` e
> `groupBy` com `DB::raw`). Em MySQL/SQLite o comportamento muda.

---

## 2. Fixtures e factories necessárias

```php
// database/factories/RelatorioEstatisticoOuvidoriaFactory.php
class RelatorioEstatisticoOuvidoriaFactory extends Factory
{
    protected $model = RelatorioEstatisticoOuvidoria::class;

    public function definition(): array
    {
        return [
            'ano'       => fake()->numberBetween(2015, 2026),
            'descricao' => fake()->sentence(4),
            'arquivo'   => 'docs/'.fake()->uuid().'.pdf',
        ];
    }
}
```

Fixtures de apoio (sem factories hoje na v5):

| Fixture | Campos mínimos | Uso |
|---|---|---|
| `OmbudsmanRequest` | `protocol`, `status_id`, `type_id`, `subject_id`, `gender`, `created_at`, `updated_at` | base de todos os agregados |
| `OmbudsmanAnswer` | `ombudsman_request_id`, `answer`, `created_at` | habilita os contadores de Atendido/Indeferido |
| `OmbudsmanManifestationType` | `title` | dimensão do gráfico por tipo |
| `OmbudsmanSubject` | `title`, `secretary_id`, `email`, `responsible` | dimensão do agregado por assunto |
| `Status` | `name` | rótulos exibidos |
| `Configuration` | `key = 'full_list_requests_disabled'`, `key = 'layout'` | chaves de comportamento |
| `StaticPage` | `slug = 'relatorio-estatistico-ouvidoria'`, `title`, `text`, `clicks` | página pública |
| Usuário Sentinel + papéis | `super-admin`, papel restrito | testes de permissão |

---

## 3. Casos de teste — Face CMS

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | Listagem exige login | sem sessão | `GET /cms/relatorios-estatisticos-ouvidoria` | redirect para `dashboard.index` (não 200) |
| C2 | Listagem mostra a tela | usuário autenticado | `GET /cms/relatorios-estatisticos-ouvidoria` | 200 + view `prefecture.relatorio-estatistico-ouvidoria.index` |
| C3 | Criar relatório sem PDF | usuário autenticado | `POST /cms/relatorios-estatisticos-ouvidoria` com `ano=2025`, `descricao=Relatório 2025` | 302 para `relatorio-estatistico-ouvidoria-cms.index`; registro criado com `arquivo = null`; flash de sucesso |
| C4 | Criar relatório com PDF | idem + `arquivo` fake (`application/pdf`) | idem | `arquivo` começa com `docs/`; arquivo existe em `storage/app/public/docs` |
| C5 | Recusar arquivo não-PDF | `arquivo` fake `text/plain` | `POST` | 302 de volta; mensagem "O arquivo deve ser um PDF."; nenhum registro criado |
| C6 | Recusar PDF acima de 20 MB | `arquivo` fake com 21 MB | `POST` | 302 de volta; mensagem "O arquivo não pode ultrapassar 20MB." |
| C7 | `ano` e `descricao` obrigatórios | POST sem ambos | `POST` | erros de validação para `ano` e `descricao` |
| C8 | Editar mantém o PDF anterior | registro com `arquivo=docs/x.pdf` | `PUT` sem novo arquivo | `arquivo` continua `docs/x.pdf` |
| C9 | Editar substitui o arquivo | registro com `arquivo=docs/x.pdf` | `PUT` com novo PDF | `arquivo` passa a apontar para o novo nome |
| C10 | Excluir é lógico | registro existe | `DELETE /cms/relatorios-estatisticos-ouvidoria/excluir/{id}` | 302; `deleted_at` preenchido; linha não aparece no DataTable |
| C11 | Botões por permissão | usuário sem a permissão `.edit` | `GET /cms/datatable/relatorio-estatistico-ouvidoria` | JSON com `edit = false` e `delete = false` |
| C12 | Numeração da coluna | 3 registros | `GET` no endpoint do DataTable | campo `value` = 1, 2, 3 na ordem `ano desc` |

---

## 4. Casos de teste — Face Portal

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Página sem filtro | solicitações em 2024 e 2025 | `GET /relatorio-estatistico-ouvidoria` | 200; view do template ativo; `$ombudsmanRequests` com todas as solicitações |
| P2 | Filtro por ano | solicitações em 2024 e 2025 | `GET ...?ano=2024` | 200; apenas as de 2024 na listagem; `$filtro_ano = 2024` |
| P3 | Lista de anos | solicitações em 2024 e 2025 | `GET` | `$datas` contém 2024 e 2025, sem repetição |
| P4 | Contagem dentro do prazo | pedido criado há 10 dias, status Atendido, com resposta | `GET` | `atendido_no_prazo = 1`, `atendido_apos_prazo = 0` |
| P5 | Contagem fora do prazo | pedido criado há 25 dias, status Atendido, com resposta | `GET` | `atendido_apos_prazo = 1`, `atendido_no_prazo = 0` |
| P6 | Limite exato de 20 dias | pedido criado há 20 dias | `GET` | classificado como **dentro** do prazo (`<= 20`) |
| P7 | Atendido sem resposta não conta | status Atendido, sem `ombudsman_answers` | `GET` | `atendido_no_prazo + atendido_apos_prazo = 0` |
| P8 | Em tramitação usa `updated_at` | status 1, `created_at` antigo e `updated_at` recente | `GET` | classificado como dentro do prazo |
| P9 | Normalização de gênero | solicitações com `gender = 'Masculino'`, `'Feminino'`, `'Outro'`, `null` | `GET` | séries: "Masculino", "Feminino" e "Não informado" (soma de "Outro" + `null`) |
| P10 | Gráfico por tipo | 2 tipos com 3 e 1 solicitações | `GET` | `$porTipo` ordenado por total decrescente |
| P11 | Agregado por assunto | 2 assuntos | `GET` | `$porAssunto` com os totais por assunto |
| P12 | Listagem completa desabilitada | `configurations.full_list_requests_disabled = 1` | `GET` | a view não renderiza a tabela `#solicitations` |
| P13 | Exportação XLSX/CSV | 1 solicitação | `GET /relatorio-estatistico-ouvidoria/exportacao?type=xls` e `?type=csv` | download com `Content-Disposition`; arquivo não vazio |
| P14 | Exportação PDF | idem | `GET ...?type=pdf` | PDF gerado (assinatura `%PDF`) |
| P15 | Exportação TXT mascarada | solicitação com nome "Maria Silva" | `GET ...?type=txt` | conteúdo contém o nome **mascarado**, não "Maria Silva" |
| P16 | Modal de solicitação | 1 solicitação com 2 respostas | `POST /ajax/get-answers/ouvidoria` | JSON com `request.protocol`, `request.mask_description` e 2 itens em `answers` |
| P17 | Página estática inexistente | sem `static_pages` com o slug | `GET` | comportamento atual: a view é renderizada e falha ao acessar `$content->layout` (ver `08-GAPS-E-DIVIDAS.md`) — o teste v7 deve exigir página amigável |
| P18 | Relatórios PDF listados | 2 registros com `ano` distintos | `GET` | a tabela de PDFs é renderizada; filtrar por `ano` mostra apenas o relatório do ano |
| P19 | Contador de cliques | página estática com `clicks = 10` | `GET` | `clicks` passa a 11 |

---

## 5. Testes unitários do service (v7)

| # | Cenário | Entrada | Esperado |
|---|---|---|---|
| U1 | Normalização de gênero | `['Masculino', 'X', null]` | `['Masculino' => 1, 'Não informado' => 2]` |
| U2 | Prazo dentro | diferença de 19 dias | `dentro = true` |
| U3 | Prazo fora | diferença de 21 dias | `dentro = false` |
| U4 | Ano sem registros | ano inexistente | todos os contadores = 0; listas vazias |
| U5 | Ordenação por total | série `{10, 3, 7}` | ordenado decrescente |

---

## 6. Como rodar

Legado (v5) — **não há testes deste módulo**; a suíte disponível é:

```bash
cd v5
./vendor/bin/phpunit
./vendor/bin/phpunit --testsuite=Feature
```

Reconstrução (v7, Laravel 11/12):

```bash
# suíte completa
php artisan test

# apenas este módulo
php artisan test --filter=RelatorioEstatisticoOuvidoria

# feature (HTTP)
php artisan test tests/Feature/RelatorioEstatisticoOuvidoria

# unit (service)
php artisan test tests/Unit/RelatorioEstatisticoServiceTest
```

Regras de ambiente para a suíte:

| Variável | Valor |
|---|---|
| `DB_CONNECTION` | `pgsql` |
| `APP_ENV` | `testing` |
| `FILESYSTEM_DISK` | `public` (o teste de upload depende disso) |
| `QUEUE_CONNECTION` | `sync` |

---

## 7. Checklist de validação manual do legado (v5)

1. `/cms/relatorios-estatisticos-ouvidoria` → DataTable carrega, colunas "Nº / Ano / Descrição / Documento / Ações".
2. Cadastrar com PDF → linha aparece com ícone de PDF e download; abrir o link `/storage/docs/...` responde o arquivo.
3. Cadastrar com arquivo `.txt` → erro "O arquivo deve ser um PDF."
4. Excluir → some da listagem; o registro continua no banco com `deleted_at` preenchido.
5. `/relatorio-estatistico-ouvidoria` → cartões, 3 gráficos e tabela de PDFs.
6. Filtrar por ano → listagem e gráficos refletem apenas o ano.
7. Botão "Limpar filtro" → **verificar se leva à página de LAI** (comportamento atual, ver `08-GAPS-E-DIVIDAS.md`).
8. Exportar em xls, pdf, csv e txt → 4 downloads válidos; TXT com dados mascarados.
9. Clicar em "Solicitação" → modal com protocolo, descrição e respostas.
10. Repetir a página com os 6 templates (`configurations.layout` = modelo1..modelo6) → mesmo conteúdo.
