# Requerimentos — Estratégia de Testes

> **Versão analisada:** `v6` (Laravel 5.5, PHPUnit) — módulo **novo na v6**
> **Referência de migração:** v7 (PHP 8.x + Laravel 11/12, Pest ou PHPUnit)

---

## 1. Estratégia

| Camada | Tipo | Ferramenta | Alvo |
|---|---|---|---|
| Portal (HTTP) | Feature | PHPUnit (`v6/tests/Feature`) | Rotas `requerimentos.index`, `.show`, `.view`, filtros, paginação, Ajax |
| CMS (HTTP) | Feature | PHPUnit (`v6/tests/Feature`) | CRUD genérico com slug `requerimentos`, subcategorias, DataTable |
| Domínio | Unit | PHPUnit (`v6/tests/Unit`) | Normalização de busca, slug único, escopo “publicados” |
| Banco | Integração | PHPUnit + migrations | Integridade da view SQL `requerimentos` e das migrations |

Configuração existente: `v6/phpunit.xml` (suites `Feature` e `Unit`,
`APP_ENV=testing`, cache/sessão em array, fila `sync`).

### Comandos

```bash
# v6 — suites
cd v6 && ./vendor/bin/phpunit --testsuite=Feature
cd v6 && ./vendor/bin/phpunit --testsuite=Unit

# v6 — um arquivo / um caso (arquivo a criar em tests/Feature/)
cd v6 && ./vendor/bin/phpunit tests/Feature/PortalRequerimentosTest.php
cd v6 && ./vendor/bin/phpunit --filter test_index_redireciona_para_primeira_categoria

# v6 — banco de teste
cd v6 && php artisan migrate --env=testing
cd v6 && php artisan db:seed --env=testing

# v7 (Pest)
php artisan test --filter=Requerimento
php artisan test --testsuite=Feature
```

> A v6 **não possui** nenhum teste do módulo Requerimentos: os casos abaixo
> são a especificação do que precisa ser coberto.

## 2. Fixtures necessárias

| Fixture | Conteúdo |
|---|---|
| `CategoryContentFactory` | Cria a raiz (`slug = 'requerimentos'`, `category_contents_id = null`) e subcategorias filhas |
| `ContentFactory` | Conteúdo genérico com `title`, `text`, `slug`, `publication_date`, `category_contents_id` |
| `CityCouncilorFactory` | Vereador com `status = true` e `name` conhecido (ex.: “Vereador Teste”) |
| `ArchiveFactory` | Anexo com `extension = 'pdf'`, `view_pdf = true` / `highlight = true` |
| `ConfigurationFactory` | `key = 'layout'`, `value = 'modelo1'` (necessário para `getTemplateActived()`) |

Pré-condições de ambiente:

- `Configuration.key = 'layout'` deve apontar para um tema **que tenha** a pasta
  `requerimentos` (`modelo1`…`modelo6`), caso contrário o controller aborta com
  `404 view_inexistente`.
- `modulos.status = true` para o módulo `requerimentos` (id 41), se o teste
  verificar a visibilidade do menu.

## 3. Casos de teste do portal

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | Raiz `requerimentos` + subcategorias “A” (order 1) e “B” (order 2) | `GET /requerimentos` | Redirect para `route('requerimentos.show', 'a')` (primeira por `order`) |
| P2 | Nenhuma subcategoria, apenas a raiz | `GET /requerimentos` | Redirect para `route('requerimentos.show', 'requerimentos')` |
| P3 | Nenhuma categoria com slug `requerimentos` | `GET /requerimentos` | Redirect para `/` com a mensagem de erro da sessão |
| P4 | Conteúdos publicados nas categorias A e B | `GET /requerimentos/a` | A resposta contém **os dois** conjuntos (listagem unificada); o título é o nome de A |
| P5 | Conteúdos em A e B | `GET /requerimentos/slug-inexistente` | Usa a primeira categoria como fallback e responde 200 |
| P6 | Conteúdos com `publication_date` em 2024, 2025 e **futuro** | `GET /requerimentos/a` | Só os dois primeiros aparecem; o futuro não é listado |
| P7 | Conteúdos em 2024 (3 meses distintos) | `GET /requerimentos/a?ano=2024` | Só os de 2024; o `select` de meses é preenchido |
| P8 | Conteúdo de março/2024 e de abril/2024 | `GET /requerimentos/a?ano=2024&mes=3` | Só o de março |
| P9 | Conteúdo com título “Requerimento de **ação** de rua” | `GET /requerimentos/a?texto=acao` | O conteúdo aparece (busca sem acento) |
| P10 | Conteúdo com título “Requerimento de **ação** de rua” | `GET /requerimentos/a?texto=acao` | O conteúdo **não** aparece (o termo não está em `title` nem em `text`) |
| P11 | 30 conteúdos publicados | `GET /requerimentos/a` | 12 itens na 1ª página e 2 itens na 3ª página; links de paginação presentes |
| P12 | Requerimento com 25 conteúdos | `GET /requerimentos/a?ano=2024` | A paginação mantém `ano=2024` nos links (`appends($request->query())`) |
| P13 | Requerimento com `clicks = 5` | `GET /requerimentos/a/slug-do-requerimento` | `contents.clicks` passa a 6 e a resposta é 200 |
| P14 | Requerimento com `publication_date` futura | `GET /requerimentos/a/slug-do-requerimento` | Redirect para `/requerimentos` |
| P15 | Slug inexistente | `GET /requerimentos/a/slug-inexistente` | Redirect para `/requerimentos` com a mensagem “Requerimento não encontrado.” |
| P16 | Requerimento com anexo `view_pdf = true` | `GET /requerimentos/a/slug-do-requerimento` | A resposta contém `<iframe` com o `path` do anexo |
| P17 | Requerimento com anexo `extension = 'pdf'` e `view_pdf = false` | `GET /requerimentos/a/slug-do-requerimento` | A resposta contém o link de download (`url($archive->path)`) |
| P18 | Conteúdo com `text` contendo HTML | `GET /requerimentos/a/slug-do-requerimento` | O HTML é renderizado sem escape (`{!! !!}`), como na v6 |
| P19 | Qualquer conteúdo de **outro** módulo com slug `x` | `GET /requerimentos/a/x` | Hoje responde 200 (fragilidade A4) — o teste v7 deve exigir **404** |
| P20 | Requisição com cabeçalho `X-Requested-With: XMLHttpRequest` | `GET /requerimentos/a?ano=2024` | Resposta é JSON cujo corpo contém o HTML do `list.blade.php` |

## 4. Casos de teste do CMS

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | Usuário `super-admin` logado | `GET /cms/conteudos/requerimentos` | 200; HTML contém a tabela `#tabela_requerimentos`; `menu_orders.clicks` aumenta em 1 |
| C2 | Usuário sem a permissão `requerimentos.create` | `GET /cms/conteudos/requerimentos` | O botão “Cadastrar Requerimento” não é renderizado |
| C3 | Payload válido (título, texto, categoria filha, data) | `POST /cms/conteudo` | Registro criado em `contents` com `category_contents_id` da subcategoria |
| C4 | Payload com `councilmen_id` de um vereador existente | `POST /cms/conteudo` | `contents.councilmen_id` gravado |
| C5 | Payload com `councilmen_id = '0'` | `POST /cms/conteudo` | `contents.councilmen_id` gravado como `NULL` |
| C6 | Payload com `councilmen_id` inexistente | `POST /cms/conteudo` | Erro de validação “O requerente selecionado é inválido.” |
| C7 | Payload sem `title` | `POST /cms/conteudo` | Erro de validação (campo obrigatório) |
| C8 | Payload com `title` de 121 caracteres | `POST /cms/conteudo` | Erro de validação (máx. 120) |
| C9 | Payload sem `text` | `POST /cms/conteudo` | Erro de validação |
| C10 | Payload sem `publication_date` | `POST /cms/conteudo` | Erro de validação |
| C11 | Anexo com extensão fora da lista (`exe`) | `POST /cms/conteudo` | Erro de validação de `mimes` |
| C12 | Requerimento existente | `PUT /cms/conteudo/{id}` com `councilmen_id` alterado | `contents.councilmen_id` atualizado |
| C13 | Requerimento existente vinculado a vereador **inativo** | `GET /cms/conteudos/editar/requerimentos/{id}` | O select de requerentes inclui o vereador inativo (preservação do vínculo) |
| C14 | Requerimento existente | `DELETE /cms/conteudo/delete/{id}` | Registro removido (soft delete) |
| C15 | 3 requerimentos marcados | `POST /cms/conteudo/deleteAll` | Os 3 são removidos |
| C16 | Requerimentos e subcategorias cadastrados | `GET /cms/datatable/requerimentos` | JSON DataTables com `data[]` contendo `titulo`, `requerente`, `categoria`, `data_publicacao`, `id`, `edit`, `delete` |
| C17 | Requerimento sem vereador | `GET /cms/datatable/requerimentos` | `requerente` vem `null` (o JS renderiza `-`) |
| C18 | Subcategoria criada via `POST /cms/categoria-conteudo` | Listagem em `/cms/categorias/requerimentos` | A nova categoria aparece; `activity_log` registrou a auditoria |
| C19 | `modulos.status = false` | `GET /cms/modulos` | O módulo aparece na lista e pode ser ligado |

## 5. Casos de teste da view SQL

| # | Arrange | Act | Assert |
|---|---|---|---|
| V1 | Conteúdo na raiz `requerimentos` (não descendente) | `SELECT * FROM requerimentos` | A raiz **não** aparece (excluída por `id NOT IN (...)`) |
| V2 | Conteúdo em subcategoria de 1º nível | `SELECT * FROM requerimentos` | Aparece, com `categoria` = nome da subcategoria |
| V3 | Conteúdo em sub-subcategoria | `SELECT * FROM requerimentos` | Aparece na view (árvore recursiva) — **divergente do portal**, que não lista |
| V4 | Requerimento com vereador ativo | `SELECT requerente FROM requerimentos` | Retorna o nome do vereador |
| V5 | Requerimento com vereador de `deleted_at` não nulo | `SELECT requerente FROM requerimentos` | Retorna `NULL` |
| V6 | Conteúdo com `deleted_at` não nulo | `SELECT * FROM requerimentos` | Não aparece |
| V7 | Conteúdo futuro | `SELECT * FROM requerimentos` | **Aparece** (a view não filtra data) — registrar como comportamento conhecido |
| V8 | Dois conteúdos criados em momentos diferentes | `SELECT value FROM requerimentos` | `value` é recalculado a cada consulta (não é chave estável) |
| V9 | Schema da view | `\d requerimentos` | Colunas: `value`, `titulo`, `data_publicacao`, `categoria`, `requerente`, `id` |

## 6. Casos unitários (v7)

| # | Alvo | Arrange / Act / Assert |
|---|---|---|
| U1 | Normalização de busca | `normalize('Requerimento de Ação')` → `requerimento de acao` (sem acentos, minúsculo) |
| U2 | Slug único | Dois requerimentos com o mesmo título → slugs `titulo` e `titulo-2` |
| U3 | Escopo `publicados` | Conteúdo com `status = publicado` e `published_at` futuro → **não** aparece no escopo |
| U4 | Escopo `publicados` | Conteúdo com `status = rascunho` e `published_at` passado → **não** aparece |
| U5 | Policy | Usuário sem `requerimentos.create` → `create()` retorna `false`; `super-admin` → `true` |
| U6 | Extensões de anexo | `Attachment::isDownloadable('pdf')` → `true`; `isDownloadable('exe')` → `false` |
| U7 | API Resource | O JSON contém `titulo`, `requerente.nome`, `categoria.nome`, `data_publicacao` no formato `dd/mm/aaaa` |

## 7. Dados de teste (fixtures de view/seed)

Para o ambiente de teste da v6, o seed mínimo é:

```php
// tests/Setup/SeedsRequerimentos.php (ilustrativo)
CategoryContent::updateOrCreate(['slug' => 'requerimentos'], [
    'name' => 'Requerimentos',
    'status_create' => 1,
]);

CategoryContent::updateOrCreate(['slug' => 'req-informacao'], [
    'name' => 'Requerimento de Informação',
    'category_contents_id' => $raiz->id,
    'order' => 1,
]);

Configuration::updateOrCreate(['key' => 'layout'], ['value' => 'modelo1']);
```

Depois, criar:

| # | Conteúdo | Categoria | `publication_date` | `councilmen_id` | `clicks` |
|---|---|---|---|---|---|
| 1 | “Requerimento de informação sobre obras” | req-informacao | 2024-03-10 10:00 | vereador ativo | 0 |
| 2 | “Requerimento de urgência” | req-informacao | 2024-04-05 09:30 | vereador ativo | 3 |
| 3 | “Requerimento de informação sobre obras” | req-informacao | 2025-03-10 10:00 | **(sem vereador)** | 0 |
| 4 | “Requerimento futuro” | req-informacao | **+1 ano** | vereador ativo | 0 |
| 5 | “Requerimento sobre ação de calçamento” | req-informacao | 2025-06-01 08:00 | vereador **removido** (`deleted_at`) | 0 |
| 6 | Conteúdo na **raiz** (não descendente) | requerimentos | 2025-01-01 08:00 | — | 0 |

Esse conjunto cobre: unificação, filtros, acento, conteúdo futuro, item sem
requerente, vereador removido e exclusão da raiz na view.

## 8. Checklist de regressão manual (v6)

1. `GET /requerimentos` → redireciona para a primeira subcategoria.
2. `/cms/conteudos/requerimentos` → tabela com título, requerente, categoria, data.
3. Cadastrar um requerimento com anexos → aparece na listagem do portal.
4. Filtrar por ano/mês/texto → lista muda sem recarregar a página.
5. Abrir um requerimento → `clicks` incrementa.
6. Baixar um anexo e conferir o `iframe` do PDF.
7. Criar uma subcategoria → aparece no menu lateral do portal.
8. Reordenar subcategorias → a ordem do menu muda.
