# 07 — Estratégia de Testes do Módulo Diário Oficial

---

## 1. Infraestrutura existente

| Item | Caminho | Observação |
|------|---------|------------|
| Configuração do PHPUnit | `v6/phpunit.xml` | Suítes `Feature` (`v6/tests/Feature`) e `Unit` (`v6/tests/Unit`); env de teste: `CACHE_DRIVER=array`, `SESSION_DRIVER=array`, `QUEUE_DRIVER=sync` |
| TestCase base | `v6/tests/TestCase.php` | — |
| Criação de aplicação | `v6/tests/CreatesApplication.php` | — |
| Factories | `v6/database/factories/` | Só existem `UserFactory.php`, `HighlightFactory.php`, `ManualAreaFactory.php`, `ManualFactory.php` |
| E2E (Playwright) | `v6/tests-e2e/` | `playwright.config.ts`, `scenarios.cjs`, `scenarios-refetch.cjs`, `fixtures/`, `histories/`, `sniff/` |
| Teste do módulo já existente | `v6/tests/Feature/DashboardDiarioWidgetTest.php` | Cobre o **widget** do dashboard, não o diário |

> **Lacuna:** o módulo Diário Oficial **não tem** testes de feature nem de
> unit no v6. A suíte E2E do Playwright foi usada na validação do upgrade
> v5 → v6 (12/12 verdes, conforme `90-LEGADO/2026-09-11-diario-upgrade-backlog.md`),
> mas não está no repositório como teste do módulo.

## 2. Estratégia

| Camada | Ferramenta | Cobertura-alvo na v7 |
|--------|-----------|----------------------|
| **Unit** | PHPUnit | Regras puras: cálculo de colunas, altura dinâmica de imagem, normalização de texto para busca, extração de trechos, formatação de data, deduplicação de slug |
| **Feature** | PHPUnit + `RefreshDatabase` | Regras de negócio que tocam banco e sessão: workflow de 4 olhos, permissões, soft delete, ordenação de matérias, filtros do portal, auditoria |
| **Integração do PDF** | PHPUnit com um PDF real em `tests/Fixtures/pdfs/` | Geração do diário: colunas, capa, assinatura, anexos, resiliência |
| **E2E** | Playwright (`v6/tests-e2e/`) | Fluxo completo do CMS e do portal |

## 3. Fixtures necessárias

| Fixture | Tipo | Descrição |
|---------|------|-----------|
| `DiarioFactory` | factory | Edição com `edicao` sequencial, `status`, `status_workflow`, `data_publicacao` |
| `DiarioAreaFactory` | factory | Área |
| `DiarioSubAreaFactory` | factory | Subárea |
| `DiarioMateriaFactory` | factory | Matéria textual ou com anexo |
| `DiarioCertificadoFactory` | factory | Certificado com `cert` e `pkey` de teste |
| `diario_configs` | seed | Registro único com `id = 1`, `logo_position = center`, `certificado_padrao` |
| `tests/Fixtures/pdfs/` | arquivos | PDF de 1 página, de 20 páginas (par), de 27 páginas (ímpar), PDF moderno com CrossReference Stream, PDF não existente/corrompido |
| `tests/Fixtures/images/` | arquivos | PNG e JPG válidos, PNG maior que 1366px de altura |
| Certificado `.pfx` de teste | arquivo | PKCS#12 com CN e senha conhecidas |

## 4. Casos de teste

### 4.1 Criação e edição da edição (Feature)

| # | Arrange | Act | Assert |
|:-:|---------|-----|--------|
| T01 | Usuário com `diario-oficial.create`; `diario_configs` presente | POST `/cms/diario-oficial` com os campos válidos | Registro criado; `status_workflow = 1`; `created_by_user_id` = usuário; `generation_status = 'pending'`; **nenhum PDF gerado**; redirect para `edit` |
| T02 | Idem T01 | POST sem `autor` | 422 com mensagem "O Autor é obrigatório." |
| T03 | Idem T01 | POST com `columns_number = 4` | 422 ("O número de colunas deve ser 1, 2 ou 3.") |
| T04 | Idem T01 | POST com `line_color = "azul"` | 422 com mensagem de hexadecimal |
| T05 | Idem T01 | POST com `archive_highlight` de 6 MB | 422 ("A imagem de capa não pode ultrapassar 5MB.") |
| T06 | Edição existente | PUT `/cms/diario-oficial/{id}` com `acao = enviar_revisao` | `status_workflow = 2`; atividade registrada |
| T07 | Edição existente | PUT com o mesmo conteúdo | Atividade registrada com "sem alteração de conteúdo" |
| T08 | Edição existente | PUT alterando `edicao` | Atividade registra o par "valor antigo → valor novo" |
| T09 | Usuário sem `diario-oficial.edit` | PUT `/cms/diario-oficial/{id}` | 403 (middleware `check.route.permission`) |

### 4.2 Workflow e publicação (Feature)

| # | Arrange | Act | Assert |
|:-:|---------|-----|--------|
| T10 | Edição com `status_workflow = 1` | PUT `/cms/diario-oficial/{id}/publish` | 422 ("Apenas edições em revisão podem ser publicadas.") |
| T11 | Edição com `status_workflow = 2`, `created_by_user_id` = usuário atual | PUT `/cms/diario-oficial/{id}/publish` | **403** (4 olhos) |
| T12 | Edição com `status_workflow = 2`, `updated_by_user_id` = usuário atual | PUT `/cms/diario-oficial/{id}/publish` | **403** |
| T13 | Edição com `status_workflow = 2`, outro usuário como autor | PUT `/cms/diario-oficial/{id}/publish` | 200; `status_workflow = 3`; `status = true`; `permission_edit = false`; `published_by_user_id` e `published_at` preenchidos; atividade "Homologação de publicação" |
| T14 | Edição com `status = false` e segregação de funções ativa | POST `/cms/diario/status` | Resposta `false`; `status` inalterado |
| T15 | Idem T14, mas a edição já está publicada | POST `/cms/diario/status` | Resposta `true`; `status = false` |

### 4.3 Matérias, áreas e subáreas (Feature)

| # | Arrange | Act | Assert |
|:-:|---------|-----|--------|
| T16 | Diário, área e subárea | POST `/cms/area` com `name` | Área criada |
| T17 | Idem T16 | POST `/cms/area` com `conteudo` e `type = text` | Matéria criada em `diario_has_areas`; `type = 'text'` |
| T18 | Idem T16 | POST `/cms/area` com `type = file` e `file` não PDF | 422 ("O arquivo anexado deve ser um PDF.") |
| T19 | Idem T16 | POST `/cms/area` com `type = file` e PDF de 6 MB | 422 ("O arquivo anexado não pode ultrapassar 5MB.") |
| T20 | Matérias com `position` 0 e 1 | PUT `/cms/ajax/diario-oficial/{id}/sort` invertendo as posições | `position` atualizado; atividade "Reordenação de matérias"; `generation_status = 'pending'` |
| T21 | `SortRequest` com `areas.*.id` inexistente | PUT `/cms/ajax/diario-oficial/{id}/sort` | 422 JSON com `success: false` |
| T22 | Diário com matérias | DELETE `/cms/diario-area/{id}` | Matéria com soft delete; JSON de sucesso |

### 4.4 Permissões e menu (Feature)

| # | Arrange | Act | Assert |
|:-:|---------|-----|--------|
| T23 | Usuário sem `diario-oficial.index` | GET `/cms/diario-oficial` | 403 |
| T24 | Usuário com `diario-subarea.index` e **sem** `diario-area.index` | GET `/cms/datatable/sub_areas_diario` | ⚠️ **Comportamento atual**: a tela resolve a permissão como `diario-area` e devolve 403. O teste deve documentar o comportamento **atual** e o esperado após a correção |
| T25 | Usuário `super-admin` | GET `/cms/datatable/diario_oficial` | Botão de excluir presente |
| T26 | Usuário comum | GET `/cms/datatable/diario_oficial` | Botão de excluir ausente |

### 4.5 Geração do PDF (Integração)

| # | Arrange | Act | Assert |
|:-:|---------|-----|--------|
| T27 | Edição sem `diario_configs` | Gerar o PDF | **Não há crash**; usa `logo_position = center` e a imagem padrão |
| T28 | Edição sem certificado (nem padrão nem próprio) | Gerar o PDF | Retorna `false`; `generation_status = 'failed'`; `generation_error` preenchido |
| T29 | Edição com `columns_number = 2` e 3 matérias textuais | Gerar o PDF | PDF criado; colunas divididas; nenhuma coluna com espaço residual grande |
| T30 | Matéria com anexo PDF de **27 páginas** (ímpar) e 2 colunas | Gerar o PDF | PDF com o número correto de páginas; **a última página não some** |
| T31 | Duas cópias do PDF de 27 páginas na mesma área, 2 colunas | Gerar o PDF | A segunda cópia começa em página nova; número de páginas correto |
| T32 | Anexo PDF moderno (CrossReference Stream) | Gerar o PDF | Conversão feita pelo fallback Imagick; anexo presente no diário |
| T33 | Anexo com `file_in = true` | Gerar o PDF | Anexo aparece no fluxo de colunas |
| T34 | Anexo com `file_in = false` | Gerar o PDF | Cada página do anexo ocupa uma página inteira do diário |
| T35 | Edição com capa | Gerar o PDF | Primeira página é a capa em A4 inteiro, sem cabeçalho; a numeração de conteúdo ignora a capa |
| T36 | Edição **publicada** | Gerar o PDF | PDF **assinado** digitalmente |
| T37 | Edição **rascunho** (`status = false`) | Gerar o PDF | PDF **sem** assinatura |
| T38 | — | Gerar o PDF duas vezes | Arquivo final é escrito de forma atômica; nenhum `.tmp.*` residual; nenhum `_merged_*` residual |
| T39 | Certificados A1 e A3 | Gerar o PDF | Assinatura válida, CN e CNPJ extraídos corretamente |

### 4.6 Portal (Feature)

| # | Arrange | Act | Assert |
|:-:|---------|-----|--------|
| T40 | Duas edições publicadas, uma não | GET `/diario-oficial` | Apenas as publicadas na listagem |
| T41 | Edição publicada | GET `/diario-oficial/{id}` | Viewer renderizado; navegação anterior/próxima calculada **por edição** |
| T42 | Duas edições publicadas com a **mesma data** | GET `/diario-oficial/{id}` | Navegação por edição funciona (não fica presa na mesma data) |
| T43 | Edição sem PDF em disco | GET `/diario-oficial/{id}` | `generation_status = 'pending'`; tela de espera renderizada |
| T44 | — | GET `/diario-oficial/viewer/search?term=ab` | `[]` (termo com menos de 3 caracteres) |
| T45 | Matérias com o termo "licitação" | GET `/diario-oficial/viewer/search?term=licit` | Lista com a edição correspondente; no máximo 20 itens |
| T46 | — | POST `/diario-oficial/filtro` com datas, termo e edição | Só as edições que atendem aos três critérios; `status = true` |
| T47 | Edição sem PDF | GET `/diario-oficial/viewer/{id}/file-info` | 404 ("Arquivo ainda não gerado.") |
| T48 | PDF gerado | GET `/diario-oficial/viewer/{id}/file-info` | Tamanho formatado e contagem de páginas |
| T49 | PDF com texto extraível | GET `/diario-oficial/viewer/{id}/search-pdf?term=licit` | Páginas com ocorrências e trechos de contexto |
| T50 | PDF com texto | Segunda chamada ao `search-pdf` | Cache JSON reused (arquivo em `storage/app/diario-oficial/pdf-text/`) |
| T51 | Edição com histórico de atividade | GET `/diario-oficial/{id}/auditoria` | Eventos públicos listados |
| T52 | — | GET `/diario-oficial/{id}` duas vezes na mesma sessão | Auditoria "Visualização no portal" registrada **uma vez** |

### 4.7 Unit

| # | Caso | Assert |
|:-:|------|--------|
| T53 | `normalizeForSearch` com texto acentuado | minúsculas sem acentos, offsets preservados |
| T54 | `snippet` no início, meio e fim do texto | trecho de até 120 caracteres, sem cortar caractere multibyte |
| T55 | `hexToRgb` com `#FF0000`, `#fff` (inválido) e valor vazio | `[255,0,0]` e `[0,0,0]` nos casos inválidos |
| T56 | Cálculo de altura dinâmica de imagem com 3 imagens em coluna de 180 mm | altura ≤ disponível; mínimo 20 mm; máximo 75 mm |
| T57 | Extração de texto com Ghostscript indisponível | retorna `null` sem exceção |

### 4.8 E2E (Playwright)

| # | Fluxo |
|:-:|-------|
| E01 | CMS: criar edição → salvar → conferir status "Rascunho" |
| E02 | CMS: clicar "Processar PDF" → ver progresso → preview habilitado |
| E03 | CMS: adicionar matéria de texto com imagem inline |
| E04 | CMS: adicionar anexo PDF e reordenar as matérias |
| E05 | CMS: enviar para revisão → (outro usuário) publicar |
| E06 | CMS: excluir a capa e a imagem de cabeçalho |
| E07 | Portal: filtrar por período, palavra-chave e edição; abrir o PDF; navegar entre edições; buscar no PDF |
| E08 | Portal: abrir a trilha de auditoria da edição |

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

| Seed | Efeito |
|------|--------|
| `diario_configs` com `id = 1` | Configuração global válida (necessária para a geração) |
| Áreas e subáreas padrão | Reproduz o conteúdo editorial mínimo para o PDF |
| Certificado de teste | Permite verificar a assinatura digital |
| `Configuration` com `titulo_site`, `cidade`, `estado`, `cep`, `favicon` | Cabeçalho e rodapé do PDF |
| Usuário com papel `redator-diario-oficial` | Fluxo de criação e edição |
| Usuário com papel `revisor-diario-oficial` | Fluxo de publicação |

## 6. Como rodar

```bash
# Suíte completa (a partir de v6/)
cd v6 && ./vendor/bin/phpunit

# Apenas os testes do módulo
cd v6 && ./vendor/bin/phpunit --filter Diario

# Arquivo específico
cd v6 && ./vendor/bin/phpunit tests/Feature/DiarioOficialTest.php

# E2E (Playwright)
cd v6/tests-e2e && npx playwright test
```

## 7. Checklist de cobertura mínima da v7

- [ ] Workflow de 4 olhos (T10 a T15).
- [ ] "Store não gera PDF" (T01).
- [ ] Assinatura apenas em publicados (T36, T37).
- [ ] Subárea não cria seção (T29 e verificação do PDF).
- [ ] Fallback FPDI → Imagick (T32).
- [ ] PDF ímpar em 2 colunas não perde a última página (T30, T31).
- [ ] Escrita atômica e limpeza de temporários (T38).
- [ ] Portal não expõe rascunhos (T40).
- [ ] Auditoria uma vez por sessão (T52).
- [ ] Permissão de subárea corrigida (T24).
