# Notícias — Estratégia de Testes

## 1. Estratégia

| Tipo | Ferramenta | Foco |
|---|---|---|
| Feature (portal) | Pest ou PHPUnit + `RefreshDatabase` | Rotas públicas, filtros de publicação, AJAX, cliques |
| Feature (CMS) | Pest/PHPUnit | Permissões, validação por slug, transação de criação, destaque/status |
| Unit (model) | PHPUnit | Scopes `scopePublished`, `scopeOfCategoryTree`, ordenação de galeria |
| Integração (view SQL) | PHPUnit sobre Postgres | View `noticias` refletindo soft delete e filtro de categoria |
| Contrato (API) | PHPUnit | Envelope `/api/v1/noticias` |

No legado **não existe suíte de testes** do módulo: `v6/tests/` tem 34
arquivos, todos voltados ao Dashboard (`v6/tests/Feature/Dashboard*Test.php`).
A única exceção é `v6/tests/Feature/DashboardNoticiasWidgetTest.php`, que
exercita o *widget de contagem* de notícias do Dashboard e documenta as
mesmas regras de visibilidade aqui descritas
(`testValidNewsInChildCategoryIsCounted`, `testRootCategoryContentIsNotCounted`,
`testSoftDeletedNewsIsNotCounted`). A reconstrução precisa criar a suíte do
zero.

## 2. Fixtures / factories necessárias

```php
// database/factories/ContentCategoryFactory.php
ContentCategory::factory()
    ->for(ContentCategory::factory()->state(['slug' => 'noticias', 'name' => 'Notícias']), 'parent')
    ->create(['slug' => 'categoria-noticia', 'name' => 'Cidade', 'quantidade_itens' => 6, 'layout' => 2]);
```

| Factory | Campos mínimos | Variações |
|---|---|---|
| `ContentCategoryFactory` | `name`, `slug`, `quantidade_itens`, `layout`, `status` | `->childOf($categoriaPai)`, `->root()` |
| `ContentFactory` | `title`, `text`, `slug`, `category_contents_id`, `publication_date`, `status` | `->published()`, `->draft()`, `->scheduled()`, `->highlighted()`, `->inCarousel()` |
| `ArchiveFactory` | `name`, `path`, `extension`, `contents_id`, `highlight` | `->highlight()`, `->gallery()`, `->inlinePdf()` |
| `LinkFactory` | `contents_id`, `link`, `title`, `isBlank` | `->youtube()`, `->vimeo()` |

Categorias base obrigatórias em todo teste que use `scopeOfCategoryTree`:
a raiz "Notícias" (`slug = 'noticias'`) e ao menos uma subcategoria.

## 3. Casos de teste — Portal

| # | Arrange | Act | Assert |
|---|---|---|---|
| T01 | 1 notícia publicada e 1 rascunho, ambos na subcategoria | `GET /noticias` | HTTP 200; somente a publicada no HTML |
| T02 | notícia com `publication_date` no futuro | `GET /noticias` | a notícia não aparece |
| T03 | notícia com `publication_date` NULL | `GET /noticias` | a notícia aparece (`orWhereNull`) |
| T04 | notícia com `deleted_at` preenchido | `GET /noticias` | a notícia não aparece |
| T05 | conteúdo gravado **na** categoria raiz "Notícias" (id 1) | `GET /noticias` | o conteúdo **não** aparece (regra R2) |
| T06 | 2 subcategorias, cada uma com `quantidade_itens = 3` | `GET /noticias` | no máximo 3 itens por faixa |
| T07 | notícia existente | `GET /noticia/{slug}` | HTTP 200; `clicks` incrementa em 1; `archives` da galeria em ordem `order` |
| T08 | notícia inexistente | `GET /noticia/nao-existe` | redireciona para `route('noticia.index')` |
| T09 | 1 notícia com `highlights = true` | `GET /noticias-ajax/{categoria}` | JSON com `contents` e `layout`; `contents[0].highlights = true` |
| T10 | notícia sem imagem de destaque | `GET /noticias-ajax/{categoria}` | `archives[0].path` = `storage/img/exemplos/exemplos/sem-imagem-min.png` |
| T11 | requisição Ajax da categoria A e depois da categoria B | duas chamadas a `/noticias-ajax/...` | a 2ª listagem **não** herda o filtro da 1ª (regressão do bug de sessão) |
| T12 | `request()->ajax()` falso em `/noticias-ajax/{categoria}` | chamada normal | resposta sem payload JSON (200 vazio) |
| T13 | notícia com `view_pdf = true` | `GET /noticia/{slug}` | o PDF inline é exibido |
| T14 | notícia com link de YouTube | `GET /noticia/{slug}` | o player/embed é renderizado (`embedVideo`) |
| T15 | 2 requisições simultâneas na mesma notícia | `GET /noticia/{slug}` x2 | `clicks` = 2 (teste de concorrência se o contador for atômico) |

## 4. Casos de teste — CMS

| # | Arrange | Act | Assert |
|---|---|---|---|
| T20 | usuário sem `noticias.create` | `GET /cms/conteudos/noticias` | o HTML **não** contém "Cadastrar Notícia" |
| T21 | payload sem `title` | `POST /cms/conteudo` | 302 de volta + erro em `title` |
| T22 | payload sem `publication_date` | `POST /cms/conteudo` | erro em `publication_date` |
| T23 | payload com `title` de 200 caracteres | `POST /cms/conteudo` | erro em `title` (max 150) |
| T24 | anexo `.exe` | `POST /cms/conteudo` | erro: "Os arquivos selecionados devem ser do tipo: rar, zip, ..." |
| T25 | payload válido | `POST /cms/conteudo` | 302; linha em `contents`; `clicks = 0`; `slug` gerado; log de atividade criado |
| T26 | título já usado | `POST /cms/conteudo` | slug desambiguado com `-<id>` |
| T27 | conteúdo com `menu_items` | `DELETE /cms/conteudo/{id}` | conteúdo com soft delete **e** `menu_items` removidos |
| T28 | conteúdo com `highlights = 1` | `POST /cms/marcar-destaque` | `highlights = 0` **e** `carousel = false` |
| T29 | conteúdo `status = 1` | `POST /cms/set-status` | `status = 0` |
| T30 | conteúdo rascunho | `POST /cms/set-status` | `status = 1` |
| T31 | 3 conteúdos selecionados | `POST /cms/conteudo/deleteAll` | os 3 com soft delete; resposta JSON de sucesso |
| T32 | `GET /cms/datatable/noticias` | — | JSON com `titulo`, `status_name_noticias`, `visualizacoes`, `destaque`; e os flags `edit`/`delete` conforme a permissão |
| T33 | conteúdo na raiz "Notícias" | `GET /cms/datatable/noticias` | **não** aparece (view filtra `cc.category_contents_id = 1`) |
| T34 | formulário de edição | `GET /cms/conteudos/editar/noticias/{id}` | 200; formulário preenchido; `gallery` com os arquivos (exceto PDF) |

## 5. Casos de teste — View SQL

| # | Arrange | Act | Assert |
|---|---|---|---|
| T40 | 1 conteúdo com `deleted_at` e 1 sem, ambos em subcategoria de Notícias | `select * from noticias` | apenas 1 linha |
| T41 | 1 conteúdo em categoria que **não** é filha de 1 | `select * from noticias` | não retorna |
| T42 | conteúdo com `status = false` | `select * from noticias` | retorna com `status_name_noticias = 'Rascunho'` |
| T43 | conteúdo com `publication_date` futura | `select * from noticias` | retorna (a view não filtra data) |

## 6. Casos de teste — API

| # | Arrange | Act | Assert |
|---|---|---|---|
| T50 | 2 notícias publicadas | `GET /api/v1/noticias` | `data` com 2 itens; campos `titulo`, `data_publicacao`, `descricao`, `interacoes`, `slug`, `arquivos` |
| T51 | filtro `?titulo=` | `GET /api/v1/noticias?titulo=saude` | apenas as notícias que casam (case/acento-insensitive) |
| T52 | filtro `?data_publicacao=01/01/2024_31/12/2024` | `GET /api/v1/noticias` | apenas as do período |
| T53 | `?limite=1` | `GET /api/v1/noticias` | 1 item |

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

Seed mínimo (`CategoryContentTableSeeder` + factory) para o ambiente de
teste:

```php
// tests/Seed/ContentCategories.php
CategoryContent::updateOrCreate(['slug' => 'noticias'], ['name' => 'Notícias', 'status' => true]);
$root = CategoryContent::where('slug', 'noticias')->first();
CategoryContent::updateOrCreate(['slug' => 'cidade'], [
    'name' => 'Cidade', 'category_contents_id' => $root->id,
    'quantidade_itens' => 6, 'layout' => 2, 'status' => true,
]);
```

Configurações mínimas em `configurations`:

| key | value | efeito |
|---|---|---|
| `layout` | `modelo1` | template do portal (a view precisa existir) |
| `noticias` | `0` | modo agrupado da listagem |
| `aviso_noticias` | `0` | aviso de manutenção da editoria |

## 8. Como rodar

```bash
# Suite completa
php artisan test

# Módulo Notícias
php artisan test --filter=News

# Arquivo específico
php artisan test tests/Feature/Portal/NewsIndexTest.php

# Cobertura do módulo
php artisan test --coverage --min=80 --filter="News|Content"
```

Ambiente legado (para testes manuais de regressão contra o PHP 7.2):

```bash
cd v6
docker compose exec app php artisan serve --host=0.0.0.0 --port=8080
# ou
docker compose exec app php artisan route:list --name=noticia
```

## 9. Checklist de regressão manual (aceitação)

1. `GET /cms/conteudos/noticias` responde 200 e a datatable carrega.
2. Criar notícia com data futura -> ausente em `/noticias`, presente no CMS.
3. Marcar destaque -> aparece no carrossel da home.
4. Abrir a matéria 3 vezes -> "Visualizações" = 3 no CMS.
5. Trocar de categoria via Ajax -> sem vazamento de filtro.
6. Excluir em lote -> itens somem da listagem e do menu.
7. `GET /api/v1/noticias` devolve o envelope esperado.
