# Notícias (V2) — Testes

> **Slug:** `publicacoes-noticias` · **Versão analisada:** V2.
>
> Caminhos relativos a `lotep/`. Os testes existentes são de integração com banco (PostgreSQL de teste) e usam `DatabaseTransactions`.

---

## 1. Estratégia

| Nível | Onde | Por quê |
|---|---|---|
| Feature (CMS) | `tests/Feature/V2/Noticias/NoticiasCmsControllerTest.php` | O controller concentra workflow, persistência, auditoria e flash; o teste garante o comportamento observável. |
| Feature (portal) | `tests/Feature/V2/Noticias/NoticiasPortalControllerTest.php` | Renderização pública, preview por token, capa e galeria. |
| Feature (navegação) | `tests/Feature/V2/Noticias/NoticiasNavegacaoPaginacaoTest.php` | Paginação acessível e prev/next determinístico. |
| Unidade | `tests/Unit/V2/Noticias/NewsModelTest.php` | Hooks do model, scopes, getters e soft delete. |
| Unidade | `tests/Unit/V2/Noticias/NewsSearchSenderTest.php` | Contrato dos DTOs de indexação por status. |
| Unidade | `tests/Unit/V2/Noticias/NewsUrlTest.php` | URLs nunca vazias. |

Os testes de feature **chamam o controller diretamente**, não via HTTP. Motivo declarado no cabeçalho: a rota catch-all `/{slug}` do BasicPage bloqueia rotas V2 específicas quando `V2_IS_DEFAULT=false` (padrão em testes). Consequência: **middleware, CSRF e roteamento não são exercitados** por esses testes.

## 2. Casos existentes — CMS

`tests/Feature/V2/Noticias/NoticiasCmsControllerTest.php` (432 linhas).

O `setUp()` cria `V2Role` com `permissions_v2 => ['Noticias' => 'write']`, usuário ativo via Sentinel, faz login e instancia o controller com as dependências reais. Também tenta converter `activity_log.subject_id` para varchar(36).

| Teste | Arrange | Act | Assert |
|---|---|---|---|
| `index_returns_all_news` | 1 publicada + 1 rascunho | `index(new Request())` | HTML contém ambos os títulos. |
| `index_has_categories_button_in_actions` | — | `index()` | Contém "Categorias" e a rota de categorias. |
| `edit_renders_edit_form_not_listing` | Notícia draft | `edit($id)` | Contém "Editar: " seguido do título, não contém "registro(s) encontrado(s)"; campo `gallery_ids` presente e sem `gallery_ids_ids`. |
| `store_persists_cover_and_gallery` | `GalleryMedia` + request com capa e galeria | `store($request)` | `cover_image_id` persistido; pivot contém a mídia. |
| `update_preserves_gallery_when_field_absent` | Notícia com galeria + request só com título | `update()` | Galeria intacta. |
| `store_creates_draft_news` | Request `status=draft` | `store()` | Status draft, token não nulo, autor correto, 302. |
| `store_creates_published_news` | Request `status=published` | `store()` | `published_at` preenchido, token nulo, 302. |
| `update_modifies_news` | Notícia draft | `update()` | Título/subtítulo alterados; redirect para `v2.noticias.cms.index`. |
| `approval_workflow_works` | Draft | `submitForApproval()` → `approve()` | `pending_approval` → `published`, data preenchida, token nulo. |
| `rejection_workflow_works` | Draft + motivo | `submitForApproval()` → `reject()` | Status `rejected`, motivo gravado. |
| `only_draft_can_be_submitted_for_approval` | Publicada | `submitForApproval()` | Continua publicada; resposta é redirect. |
| `cms_preview_renders` | Draft | `preview()` | HTML contém o título. |
| `destroy_soft_deletes` | Draft | `destroy()` | `assertSoftDeleted('v2_news')`. |
| `seo_update_works` | Publicada + `seo` | `updateSeo()` | `metadata.seo.title/description` atualizados. |
| `validation_title_required` | Título vazio | `store()` | 302 (erro de validação redireciona). |
| `pending_filter_works` | 1 pendente + 1 publicada marcador | `pending()` | Pendente aparece; publicada aparece **1×** (só no seletor de super-destaque). |
| `categories_store_redirects_for_form_requests` | Request HTML | `categoriesStore()` | `RedirectResponse` 302 + registro criado. |
| `categories_store_returns_json_for_ajax_requests` | Request com `Accept: application/json` | `categoriesStore()` | JSON 201 + registro. |
| `categories_destroy_redirects_for_form_requests` | Categoria | `categoriesDestroy()` | Redirect; `deleted_at` preenchido. |

## 3. Casos existentes — portal

`tests/Feature/V2/Noticias/NoticiasPortalControllerTest.php` (212 linhas).

| Teste | Arrange | Act | Assert |
|---|---|---|---|
| `public_index_lists_published_news` | 2 publicadas + 1 rascunho | `index()` | Publicadas presentes; rascunho ausente. |
| `public_show_displays_published_news` | Publicada com AST | `show($slug)` | Título e subtítulo no HTML. |
| `draft_returns_404_without_token` | Draft | `show($slug)` | `ModelNotFoundException`. |
| `preview_with_valid_token_shows_draft` | Draft + token | `preview($slug, $request)` | HTML contém título e "Modo Preview". |
| `preview_with_invalid_token_returns_404` | Draft + token errado | `preview()` | `NotFoundHttpException`. |
| `show_displays_publish_date` | Publicada em 08/07/2026 | `show()` | Contém `08/07/2026`. |
| `show_displays_cover_image_after_the_date` | Capa + `variants` | `show()` | `og:image` e `<picture>` presentes; capa aparece depois da data. |
| `show_displays_gallery_when_news_has_images` | Galeria com variante | `show()` | "Galeria de Imagens" e caminho da imagem presentes. |
| `show_always_renders_gallery_section` | Sem galeria | `show()` | Seção presente com "Nenhuma imagem na galeria desta notícia". |
| `empty_index_shows_empty_state` | Filtro por categoria inexistente | `index($request)` | "Volte mais tarde". |

## 4. Casos existentes — paginação e navegação

`tests/Feature/V2/Noticias/NoticiasNavegacaoPaginacaoTest.php` (171 linhas).

| Teste | Arrange | Act | Assert |
|---|---|---|---|
| `index_renders_pagination_when_more_than_one_page` | 13 publicadas | `index()` | `data-news-pagination`, `aria-current="page"`, `rel="next"`; item 13 fora da 1ª página. |
| `index_does_not_render_pagination_with_single_page` | `DELETE FROM v2_news` + 5 publicadas | `index()` | Sem paginação; grade presente. |
| `index_pagination_preserves_query_string` | 13 publicadas + request `?foo=bar` | `index()` | Href da paginação contém `foo=bar`. |
| `show_renders_prev_next_navigation` | Antiga/Média/Nova | `show(média)` | `aria-label` e `rel=prev/next` corretos. |
| `show_hides_prev_next_navigation_when_no_adjacent` | `DELETE FROM v2_news` + única | `show()` | Sem "Navegação entre notícias". |

## 5. Casos existentes — unidade

`tests/Unit/V2/Noticias/NewsModelTest.php` (15 testes): `test_uuid_generated_on_create`, `test_slug_generated_from_title`, `test_draft_token_generated_for_draft`, `test_no_draft_token_for_published`, `test_published_at_set_on_publish`, `test_draft_token_removed_on_publish`, `test_scopes`, `test_status_labels`, `test_status_colors`, `test_get_seo_data_with_fallbacks`, `test_get_seo_data_with_custom_metadata`, `test_is_accessible_published`, `test_is_accessible_draft_with_valid_token`, `test_unique_slug_generation`, `test_soft_delete`.

`tests/Unit/V2/Noticias/NewsSearchSenderTest.php` (3 testes): mocka `PublicIndexerService` e `CMSIndexerService` via Mockery e verifica `index`/`remove` e o conteúdo dos DTOs para publicado, draft e CMS.

`tests/Unit/V2/Noticias/NewsUrlTest.php` (4 testes): URL pública com slug, fallback sem slug, preview nunca vazia, preview com token.

## 6. Fixtures e factories necessárias

Não há factories de Notícias no repositório; os testes criam registros com `News::create($atributos)`. Para o V7, criar:

| Factory | Estado/atributos |
|---|---|
| `NoticiaFactory` | `titulo`, `slug`, `status`, `autor_id`; states `rascunho()`, `pendente()`, `publicada()`, `rejeitada()`. |
| `CategoriaFactory` | `nome`, `slug`, `ordem`; state `sistema()`, `filhaDe($pai)`. |
| `TagFactory` | `nome`, `slug` únicos. |
| `AprovadorFactory` | `usuario_id`, vigência; states `ativo()`, `vencido()`, `futuro()`. |
| `GalleryMediaFactory` | `owner_module`, `file_path`, `mime_type`, `variants` (array), `alt_text`. |

Fixtures de seed para teste: nenhuma. A base de teste usa transações e `truncate()` pontual; não depender de `V2/Seeds/BasicPages/NoticiasSeeder.php` (que cria página BasicPage, não notícia).

## 7. Comando para rodar

```bash
# a partir da raiz de lotep/
vendor/bin/phpunit --filter 'Tests\\(Feature|Unit)\\V2\\Noticias'
```

Alternativas:

```bash
vendor/bin/phpunit tests/Feature/V2/Noticias
vendor/bin/phpunit tests/Unit/V2/Noticias
vendor/bin/phpunit --filter NoticiasCmsControllerTest
```

## 8. Lacunas de teste (casos que faltam)

| # | Cenário | Por que importa |
|---|---|---|
| 1 | `update()` com `status=published` a partir de `pending_approval`/`draft` | Comprova o bypass do workflow. |
| 2 | `approve()`/`reject()` invalidam cache | Regressão de portal desatualizado. |
| 3 | `destroy()` remove do índice público | Busca não deve devolver arquivada. |
| 4 | Sitemap não inclui soft-deleted | Google News não deve listar arquivada. |
| 5 | Sitemap respeita janela de 6 meses e limite 1000 | Volume e cache. |
| 6 | Recriar tag/categoria com mesmo nome após soft delete | Evita 500 por UNIQUE. |
| 7 | Nome duplicado de categoria não trava | O laço atual pode não terminar. |
| 8 | `NewsRequest` rejeita `content` que não é JSON | Validação real do AST. |
| 9 | Middleware exige `write` para `POST /aprovar` | RBAC de rota, hoje não coberto por chamada direta. |
| 10 | Usuário `read` recebe 403/redirect em POST | Confirma o nível de permissão. |
| 11 | Super-destaque é único após duas chamadas | Consistência do singleton. |
| 12 | Preview de publicada com token inválido | Documenta o comportamento atual (abre). |
| 13 | Paginação com `published_at` empatado | Ordenação determinística da listagem. |
| 14 | Notícia sem capa/galeria/tags | Estados vazios no CMS e portal. |
| 15 | E-mail de submissão não é enviado ao próprio autor | Regra de `NewsNotificationService`. |

## 9. Observações de testabilidade

- `NoticiasCmsController::clearPageCache()` depende de Redis real; para testar invalidação, extrair uma porta (`CachePurger`) e injetar um dublê.
- O controller usa `flash()` e `session()`; chamadas diretas exigem sessão inicializada pelo `TestCase`.
- `News::saved` dispara indexação real (banco) em todos os testes do model; `NewsSearchSenderTest` contorna com mocks no container.
- `NewsModelTest::setUp()` faz `truncate()` em `v2_news`, o que é incompatível com execução paralela de testes no mesmo banco.
- `DatabaseTransactions` + `truncate()` em `v2_news` pode conflitar com FKs de pivots no PostgreSQL 18 se as constraints forem adicionadas no V7 (uso de `TRUNCATE` com `CASCADE` ou `DELETE`).
