# Publicações — Estratégia de Testes

## 1. Estratégia

| Tipo | Ferramenta | Foco |
|---|---|---|
| Feature (portal) | Pest/PHPUnit + `RefreshDatabase` | Listagem, filtros, documento, redirecionamento, exportação |
| Feature (CMS) | Pest/PHPUnit | Permissões, validação, criação com anexos, "Link Permanente" |
| Unit (model) | PHPUnit | Scope `published`, ordenação, `layout` |
| Integração (query) | PHPUnit sobre Postgres | Busca acento-insensível, filtro ano/mês |
| Contrato (API) | PHPUnit | Envelope `/api/v1/publicacoes` |

No legado não há suíte para o módulo. A única referência é
`v6/tests/Feature/DashboardPublicacoesWidgetTest.php`, que valida o widget
de contagem de publicações do Dashboard.

## 2. Fixtures / factories

```php
// database/factories/PublicationCategoryFactory.php
PublicationCategory::factory()->create(['slug' => 'editais', 'name' => 'Editais', 'order' => 1]);

// database/factories/PublicationFactory.php
Publication::factory()->for(PublicationCategory::factory()->state(['slug' => 'editais']))->create([
    'title' => 'Edital 01/2026',
    'slug' => 'edital-01-2026',
    'publication_date' => now()->subDay(),
    'status' => true,
    'layout' => PublicationLayout::WITHOUT_SIDEBAR,
]);
```

| Factory | Campos | Variações |
|---|---|---|
| `PublicationCategoryFactory` | `name`, `slug`, `order` | `->root()` |
| `PublicationFactory` | `title`, `body`, `slug`, `publication_date`, `status`, `layout` | `->published()`, `->draft()`, `->scheduled()`, `->withSidebar()` |
| `PublicationFileFactory` | `publication_id`, `name`, `path`, `role` | `->inlinePdf()` |

Categorias base obrigatórias: raiz "Publicações" + 9 subcategorias
(`documentos`, `editais`, `horarios-funcionarios`, `manuais`,
`mensario-oficial`, `concurso-publico`, `campanhas`, `diario-oficial`,
`portal-do-contribuinte`), replicating o seed
`v6/database/seeds/CategoryContentTableSeeder.php`.

## 3. Casos de teste — Portal (listagem)

| # | Arrange | Act | Assert |
|---|---|---|---|
| T01 | 9 subcategorias com `order` distintos | `GET /publicacoes` | 302 para `publicacoes.show` da subcategoria de menor `order` |
| T02 | nenhuma subcategoria | `GET /publicacoes` | erro 500 **ou** página de "sem categorias" (comportamento a definir) |
| T03 | 13 documentos publicados + 1 rascunho | `GET /publicacoes/editais` | apenas os 13 publicados; paginação de 12 |
| T04 | 1 documento com data futura | `GET /publicacoes/editais` | não aparece |
| T05 | documento futuro + requisição com `mes` | `GET /publicacoes/editais?mes=3` | **aparece** (comportamento atual — regra P3) |
| T06 | 3 documentos (2024-03, 2024-07, 2025-01) | `GET /publicacoes/editais?ano=2024&mes=3` | apenas o de 2024-03 |
| T07 | 3 documentos (2024-03, 2024-07, 2025-01) | `GET /publicacoes/editais?mes=3` | apenas o de 2024-03 (mês sem ano **não** filtra — regra P4) |
| T08 | título "Saúde Municipal" | `GET /publicacoes/editais?texto=saude` | o documento aparece (busca sem acento) |
| T09 | título "100% convênio" | `GET /publicacoes/editais?texto=100` | o `clean_slug` remove o `%` — comportamento a registrar |
| T10 | 15 documentos | `GET /publicacoes/editais?page=2` | página 2 com 3 itens; links de paginação |
| T11 | requisição Ajax com `?page=2` | `GET /publicacoes/editais?page=2` (XHR) | JSON com o HTML renderizado da partial `list` |
| T12 | 2 categorias com documentos | `GET /publicacoes/editais` | a navegação lateral lista as duas categorias |

## 4. Casos de teste — Portal (documento)

| # | Arrange | Act | Assert |
|---|---|---|---|
| T20 | documento publicado com `layout = 1` | `GET /publicacoes/editais/{slug}` | 200; barra lateral presente; `clicks` +1 |
| T21 | documento com `layout = 2` | `GET /publicacoes/editais/{slug}` | 200; barra lateral **ausente** |
| T22 | documento com `layout = NULL` | `GET /publicacoes/editais/{slug}` | barra lateral presente (regra P11) |
| T23 | documento com anexo | `GET /publicacoes/editais/{slug}` | o arquivo é listado |
| T24 | documento com `view_pdf = true` | `GET /publicacoes/editais/{slug}` | o PDF inline é exibido |
| T25 | documento com data futura | `GET /publicacoes/editais/{slug}` | 302 para `/publicacoes` |
| T26 | documento **rascunho** | `GET /publicacoes/editais/{slug}` | 200 e conteúdo visível (regra P9 — não corrigido) |
| T27 | slug inexistente | `GET /publicacoes/editais/nao-existe` | erro 500 (acesso a `null->publication_date`) |
| T28 | mesma slug em 2 categorias | `GET /publicacoes/categoriaA/{slug}` | abre o documento da **categoria B** (regra P8) |
| T29 | 2 acessos simultâneos | `GET /publicacoes/editais/{slug}` x2 | `clicks` = 2 (com contador atômico) |

## 5. Casos de teste — exportação

| # | Arrange | Act | Assert |
|---|---|---|---|
| T30 | 3 documentos em `editais` | `GET /publicacoes/export?sub_category=editais&type=csv` | 200; `Content-Disposition` com `publicacoes_editais_*.csv`; 3 linhas + cabeçalho |
| T31 | idem | `GET /publicacoes/export?sub_category=editais&type=xls` | arquivo `.xlsx` |
| T32 | idem | `GET /publicacoes/export?sub_category=editais&type=txt` | arquivo `.txt` (TSV) |
| T33 | idem | `GET /publicacoes/export?sub_category=editais&type=pdf` | PDF A4 paisagem; cabeçalho "Publicações - Editais" |
| T34 | `sub_category` inexistente | `GET /publicacoes/export?sub_category=xxx&type=csv` | **404** |
| T35 | `sub_category` ausente | `GET /publicacoes/export?type=csv` | **404** |
| T36 | documentos de 2024 e 2025 | `GET /publicacoes/export?sub_category=editais&ano=2024&type=csv` | só os de 2024 |
| T37 | `type` desconhecido | `GET /publicacoes/export?sub_category=editais&type=xml` | cai no padrão CSV |
| T38 | `configurations.cor_1` ausente | `GET /publicacoes/export?sub_category=editais&type=pdf` | PDF gerado com a cor padrão `#1075ad` |

## 6. Casos de teste — CMS

| # | Arrange | Act | Assert |
|---|---|---|---|
| T40 | usuário sem `publicacoes.create` | `GET /cms/conteudos/publicacoes` | o HTML não contém "Cadastrar Publicação" |
| T41 | payload sem `layout` | `POST /cms/conteudo` | erro em `layout` |
| T42 | payload sem `category_contents_id` | `POST /cms/conteudo` | erro em `category_contents_id` (SelectRule) |
| T43 | payload com `layout = 3` | `POST /cms/conteudo` | erro (numérico, mas fora do enum) |
| T44 | payload válido | `POST /cms/conteudo` | 302; documento criado; `clicks = 0`; `layout` gravado |
| T45 | documento existente | `GET /cms/conteudos/editar/publicacoes/{id}` | o HTML mostra `/publicacoes/{categoria}/` + a slug; link "Acessar" |
| T46 | anexo `.exe` | `POST /cms/conteudo` | erro "Os arquivos selecionados devem ser do tipo: rar, zip, ..." |
| T47 | anexo `pdf` | `POST /cms/conteudo` | `archives` criado com `static_pages_id` NULL e `contents_id` preenchido |
| T48 | `GET /cms/datatable/publicacoes` | — | JSON com `titulo`, `categoria`, `categoria_slug`, `data_publicacao`, `slug`, `id` |
| T49 | 2 categorias com documentos | `GET /cms/datatable/publicacoes` | a coluna `categoria` mostra o nome da subcategoria |
| T50 | exclusão em lote | `POST /cms/conteudo/deleteAll` | soft delete + remoção dos `menu_items` |

## 7. Casos de teste — API

| # | Arrange | Act | Assert |
|---|---|---|---|
| T60 | 2 publicações | `GET /api/v1/publicacoes` | `data` com 2 itens e os campos `titulo`, `data_publicacao`, `descricao`, `interacoes`, `slug`, `arquivos` |
| T61 | filtro `?titulo=` | `GET /api/v1/publicacoes?titulo=edital` | apenas as que casam |
| T62 | `?data_publicacao=01/01/2024_31/12/2024` | `GET /api/v1/publicacoes` | apenas as do período |
| T63 | `?limite=1` | `GET /api/v1/publicacoes` | 1 item |

## 8. Fixtures de view/seed para o ambiente de teste

```php
// tests/Seed/PublicationCategories.php
$root = PublicationCategory::updateOrCreate(['slug' => 'publicacoes'], ['name' => 'Publicações', 'order' => 0]);
foreach ([
    'documentos' => 'Documentos',
    'editais' => 'Editais',
    'horarios-funcionarios' => 'Horários Funcionários',
    'manuais' => 'Manuais',
    'mensario-oficial' => 'Mensário oficial',
    'concurso-publico' => 'Concurso Público',
    'campanhas' => 'Campanhas',
    'diario-oficial' => 'Diário oficial',
    'portal-do-contribuinte' => 'Portal do Contribuinte',
] as $slug => $name) {
    PublicationCategory::updateOrCreate(['slug' => $slug], ['name' => $name, 'parent_id' => $root->id]);
}
```

Configurações necessárias em `configurations`:

| key | value | efeito |
|---|---|---|
| `layout` | `modelo1` | template do portal (a view precisa existir) |
| `cor_1` | `#1075ad` | cor do PDF de exportação |

## 9. Como rodar

```bash
php artisan test
php artisan test --filter=Publication
php artisan test tests/Feature/Portal/PublicationExportTest.php
php artisan test --coverage --min=80 --filter=Publication
```

Ambiente legado, para regressão manual:

```bash
cd v6
docker compose exec app php artisan route:list --name=publicacoes
```

## 10. Checklist de regressão manual

1. `/publicacoes` redireciona para a primeira subcategoria.
2. Filtro por ano, ano+mês e texto funcionam.
3. A exportação gera os 4 formatos com o nome esperado.
4. `sub_category` inválido devolve 404.
5. O documento abre com anexos e PDF inline, com ou sem barra lateral.
6. O "Link Permanente" aparece na edição e abre o documento correto.
7. O "Acessar publicação" na datatable abre em nova aba a URL correta.
8. A breadcrumb do documento leva à **categoria** (comportamento atual) —
   registrar se é intencional.
