# Transparência Fiscal — Testes

> Estratégia proposta para a reconstrução (v7) e linha de base do que hoje **não** tem
> cobertura no legado.

---

## 1. Situação atual

O repositório `v5/`/`v6/` **não possui suíte de testes** para este módulo: não há
`tests/Feature`/`tests/Unit` cobrindo `Portal\TransparencyController`,
`Prefecture\ContentController` (case `transparencia-fiscal`),
`Prefecture\DatatableController@get_transparencia_fiscal` nem a view
`transparencia_fiscal`.

Consequência: qualquer refatoração do relatório de LAI ou da ordenação da grade de
transparência **não tem rede de segurança**. A v7 deve começar com testes de
**caracterização** (replicar o comportamento atual) antes de refatorar.

---

## 2. Estratégia

| Tipo | Escopo | Ferramenta |
|---|---|---|
| Feature (portal) | Rotas públicas: índice, categoria, item, relatório, Ajax de respostas | Pest ou PHPUnit + `RefreshDatabase` |
| Feature (CMS) | CRUD de itens e categorias, DataTable, permissões | PHPUnit + `actingAs` (Sentinel) |
| Unit | Regras de prazo, ordenação, máscara, montagem do TXT | PHPUnit puro |
| Contrato de API | Formato do JSON de `getAnswers` e do payload do DataTable | Pest |
| Regressão de view | Existência de `transparencia-fiscal.*` em **todos** os templates | Teste de filesystem (evita a regressão do `subcategorias.blade.php`) |

Camadas de banco: **SQLite em memória** para a maioria dos testes; **PostgreSQL** (via
`docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5`) apenas para
os testes que dependem da view SQL e das funções `EXTRACT(DAY FROM ...)`, que não
existem no SQLite.

---

## 3. Fixtures / factories necessárias

| Factory | Dados mínimos |
|---|---|
| `TransparencyCategoryFactory` | `name`, `slug`, `parent_id = null` (raiz) ou apontando para a raiz, `position`, `description` |
| `TransparencyItemFactory` | `category_id`, `title`, `slug`, `icon`, `url`, `open_in_new_tab`, `legal_reference`, `position`, `status` |
| `InformationRequestFactory` | `protocol`, `request`, `masked_request`, `status_id`, `person_id`, `created_at` (dentro e fora do ano filtrado) |
| `InformationRequestAnswerFactory` | `information_request_id`, `answer`, `created_at` (data que define dentro/fora do prazo) |
| `PersonFactory` / `StatusFactory` | Nome do solicitante; status 1-4 |
| `ConfigurationFactory` | `layout`, `full_list_requests_disabled`, `portal_transparencia_*` |
| `StaticPageFactory` | `slug = 'relatorio-estatistico'`, `title`, `text`, `layout` |
| `ArchiveFactory` | `static_pages_id`, `highlight = null`, `view_pdf` |

### 3.1 Seeders de teste (ambiente)

| Seeder | Papel |
|---|---|
| `v6/database/seeds/AddPortalTransparenciaConfigurationsSeeder.php` | Cria as chaves `portal_transparencia_*` (idempotente) |
| `v6/database/seeds/AddTerceirizadosInTransparenciaFiscalSeeder.php` | Cria o item "Terceirizados" **somente** se `configurations.estado = 'PB'` — em ambiente de teste, fixar `estado` conforme o cenário |

> A raiz `category_contents` com `slug = 'transparencia-fiscal'` (id 25) precisa existir no
> seed de teste, ou a v7 precisa de um seeder dedicado `TransparencySeeder` que a crie.

---

## 4. Casos de teste — Portal

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Índice renderiza | Categoria raiz + 2 subcategorias + 3 itens ativos e 1 inativo | `GET /transparencia-fiscal` | 200; Blade contém os 3 títulos ativos; **não** contém o item inativo |
| P2 | Seção "Geral" | 1 item na raiz, 2 em subcategoria | `GET /transparencia-fiscal` | O item da raiz aparece na seção Geral; os das subcategorias aparecem sob o nome da categoria |
| P3 | Descrição da categoria | Categoria com `description` | `GET /transparencia-fiscal` | A descrição é renderizada abaixo do título da seção |
| P4 | Ordem dos itens | Itens com `position` 3, 1, 2 | `GET /transparencia-fiscal` | A ordem exibida é 1, 2, 3 |
| P5 | Slug inexistente na listagem | — | `GET /transparencia-fiscal/nao-existe` | 500 **hoje** (acesso a propriedade de `null`) — na v7 deve ser 404 |
| P6 | Item com `open_in_new_tab` | Item com URL externa e flag true | `GET /transparencia-fiscal` | O HTML contém `target="_blank"` e a URL direta |
| P7 | Item sem URL externa | Item com `open_in_new_tab` false | `GET /transparencia-fiscal` | O link aponta para a rota interna `transparencia.show` |
| P8 | Página do item | Item com `url` | `GET /transparencia-fiscal/{slug}` | 200; o HTML contém `<iframe src="{url}">` |
| P9 | Categoria válida | Subcategoria da raiz | `GET /transparencia/categoria/{slug}` | 200; lista os itens da categoria |
| P10 | Categoria inválida | Slug desconhecido | `GET /transparencia/categoria/xxx` | 302 para `route('transparencia.index')` |
| P11 | Categoria fora da raiz | Categoria de outro módulo | `GET /transparencia/categoria/{slug}` | 302 para o índice (a categoria não é filha da raiz) |
| P12 | Relatório sem ano | — | `GET /relatorio-estatistico` | 200; gráfico renderizado com 8 fatias |
| P13 | Relatório com ano | Pedidos em 2024 e 2025 | `GET /relatorio-estatistico?ano=2024` | Só os pedidos de 2024 na tabela; `<option value="2024" selected>` |
| P14 | Contadores de prazo | 1 pedido "Em Tramitação" com `created_at` há 25 dias; 1 com há 5 dias | `GET /relatorio-estatistico?ano=2024` | `emTramitacao_apos_prazo = 1`, `emTramitacao_no_prazo = 1` |
| P15 | Prazo de resposta | Pedido "Atendido" com resposta 30 dias depois | `GET /relatorio-estatistico?ano=2024` | `atendido_apos_prazo = 1` |
| P16 | Prazo prorrogado | Pedido status 4 com `updated_at` há 30 dias | `GET /relatorio-estatistico?ano=2024` | `prorrogado_apos_prazo = 1` (usa `updated_at`, não `created_at`) |
| P17 | Exportação CSV | 2 pedidos | `GET /relatorio-estatistico?ano=2024&type=csv` | 200; `Content-Type` de planilha; corpo com os protocolos mascarados |
| P18 | Exportação XLS | idem | `GET /relatorio-estatistico?type=xls` | Arquivo `relatorio-estatistico.xlsx` |
| P19 | Exportação PDF | idem | `GET /relatorio-estatistico?type=pdf` | Arquivo `relatorio-estatistico.pdf` |
| P20 | Exportação TXT | idem | `GET /relatorio-estatistico?type=txt` | Corpo texto com cabeçalho "LAI - Relatório Estatístico" e os protocolos mascarados |
| P21 | Listagem completa desabilitada | `full_list_requests_disabled = 1` | `GET /relatorio-estatistico` | A tabela `#solicitations` **não** é renderizada; o gráfico permanece |
| P22 | Incremento de cliques | `static_pages` do relatório | `GET /relatorio-estatistico` | `clicks` aumenta em 1 |
| P23 | Ajax de respostas | Pedido com 2 respostas e 1 anexo | `POST /AjaxGetanswers` com `id` | 200; JSON com `request.protocol` mascarado, `request.mask_request`, `request.status_id` e `answers` com 2 itens |
| P24 | Ajax com id inexistente | — | `POST /AjaxGetanswers` com `id = 999999` | 204 e corpo "Ocorreu um erro." |
| P25 | Manutenção ativa | `pagina_em_manutencao = 1` | `GET /transparencia-fiscal` | Bloqueado pelo middleware `check.site.manutention` |
| P26 | URIs alias | — | `GET /transparencia`, `/portal-da-transparencia`, `/portal-transparencia` | 200 e conteúdo idêntico ao de `/transparencia-fiscal` |

---

## 5. Casos de teste — CMS

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | Listagem do CMS | Usuário com permissão | `GET /cms/conteudos/transparencia-fiscal` | 200; renderiza a tabela `#tabela_transparencia_fiscal` |
| C2 | DataTable | 3 itens (2 ativos, 1 inativo) | `GET /cms/datatable/transparencia_fiscal` | JSON com `data` de 3 linhas; `titulo`, `category_name`, `status_name` presentes; `status_name` = "Ativado"/"Desativado" |
| C3 | Valor da coluna `value` | Itens com `order` 5 e 2 | `GET /cms/datatable/transparencia_fiscal` | `value` = 1 para o de `order` 2 e 2 para o de `order` 5 (numeração por `order`, não por id) |
| C4 | `category_name` | Item na raiz e item em subcategoria | idem | O da raiz traz o nome da raiz; o da subcategoria traz o nome da subcategoria |
| C5 | Criar item | Formulário válido | `POST /cms/conteudos` | `contents` gravado com `title`, `icon`, `order`, `status`; `links` gravado com `link` e `isBlank` |
| C6 | Reordenação na criação | 2 itens com `order` 1 e 2; cria item com `order` 2 | `POST /cms/conteudos` | Os existentes passam a `order` 2 e 3; o novo fica com 2 |
| C7 | Item na raiz | `category_contents_id = 0` | `POST /cms/conteudos` | O item é gravado com o id da categoria raiz (não 0) |
| C8 | Validações | Título vazio, sem ícone, sem URL | `POST /cms/conteudos` | 422; erros em `title`, `icon`, `link` |
| C9 | Referência | `referencia = "2.1 a 2.5"` | `POST /cms/conteudos` | Campo gravado (≤ 50) |
| C10 | Slug duplicado | Já existe conteúdo com o mesmo título | `POST /cms/conteudos` | Slug recebe sufixo numérico; as duas URLs coexistem |
| C11 | Excluir em lote | 3 itens marcados | `POST /cms/ajax/conteudos/deleteAll` | Os 3 são removidos (soft delete) |
| C12 | Categoria sem `description` | `description` vazia | `POST /cms/categoria-conteudo` | Aceito (regra `nullable|max:500` só para a raiz da transparência) |
| C13 | Categoria com pai correto | Categoria dentro de `transparencia-fiscal` | `POST` | `description` persistida |
| C14 | Não excluir a raiz | — | `DELETE /cms/categoria-conteudo/25` | 302 para `cadastrar-categorias`; a raiz continua existindo |
| C15 | Permissões | Usuário logado sem `transparencia-fiscal.create` | `GET /cms/conteudos/cadastrar/transparencia-fiscal` | Redirect para a rota anterior com o flash "Você não tem autorização para acessar esta funcionalidade!" (o middleware `check.route.permission` **não** devolve 403) |
| C16 | Flags do DataTable | Usuário sem `transparencia-fiscal.edit` | `GET /cms/datatable/transparencia_fiscal` | `edit = false`, `delete = false` nas linhas |

---

## 6. Testes unitários

| # | Alvo | Assert |
|---|---|---|
| U1 | `DataMaskTrait@maskProtocol` | Protocolo parcialmente mascarado, mantendo o formato |
| U2 | `DataMaskTrait@maskName` | Primeiro nome visível, restante mascarado |
| U3 | `InformationsRequest::checkParameters` | Status 1/2/3 ⇒ `created_at`; status 4 ⇒ `updated_at`; comparação 1 ⇒ `>`, 2 ⇒ `<=` |
| U4 | `exportTxt` | Cabeçalho + uma linha por pedido, com status, protocolo mascarado, nome mascarado e data `d/m/Y` |
| U5 | `ContentPresenter@textColor` | 1→warning, 2→success, 3→danger, 4→primary |
| U6 | `PortalTransparenciaService@isActive` | Falso se `portal_transparencia_ativo != 1` ou `codigo_ente` vazio; verdadeiro com ambos |
| U7 | `PortalTransparenciaService@getConfig` | Usa cache (segunda chamada não toca o banco) |

---

## 7. Testes de regressão estrutural (importantes)

| # | Teste | Assert |
|---|---|---|
| S1 | Existência das views por template | Para cada template em `modelo1..modelo6` (e `newsbit`), existem `transparencia-fiscal/index.blade.php`, `show.blade.php`, `subcategorias.blade.php`, `relatorio-estatistico/index.blade.php` e `relatorio-estatistico/js.blade.php` |
| S2 | Nomes de rota únicos | Não existem dois registros com o mesmo `->name()` (`transparencia.index`, `transparencia.show`) |
| S3 | Colunas do DataTable × JS | As chaves usadas em `datatable.js` (`titulo`, `category_name`, `status_name`) existem no payload do controller |
| S4 | Colunas da view SQL × JS | `pg_get_viewdef` contém `titulo`, `category_name`, `status_name` |
| S5 | Colunas da view SQL × Resource | O Resource da v7 expõe os mesmos campos |

> S1 e S2 cobrem exatamente as duas falhas já observadas no legado (ausência de
> `subcategorias.blade.php` fora do `modelo5` e URIs duplicadas).

---

## 8. Como rodar

Legado (não há testes; a verificação é manual):

```bash
# Rota de referência do portal
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/transparencia-fiscal

# Verificar a view SQL no banco (read-only)
docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 \
  -c "select * from transparencia_fiscal order by value limit 20;"
```

V7 (proposto):

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

# apenas o módulo
php artisan test --filter=Transparency

# feature do portal com banco PostgreSQL
php artisan test --testsuite=Feature --filter=TransparencyReportTest

# factories
php artisan db:seed --class=Database\\Seeders\\TransparencySeeder
```

Configuração sugerida no `phpunit.xml`:

```xml
<env name="DB_CONNECTION" value="pgsql"/>
<env name="DB_DATABASE" value="prefeituras_v5_test"/>
```
