# PLATAFORMA — Testes

> Situação real medida no repositório: `v6/tests/` (PHPUnit 6) e
> `v6/tests-e2e/` (Playwright + sniffCSS).

---

## 1. O que existe hoje

### 1.1 `v6/tests/` — PHPUnit

**34 arquivos `.php`**, sendo:

| Suíte | Arquivos | Foco |
|---|---|---|
| `v6/tests/Feature/` | 27 | Dashboard (todos), configuração GA, widgets |
| `v6/tests/Unit/` | 4 | `DashboardGridModelTest`, `DashboardUnitTest`, `GoogleAnalyticsServiceTest`, `ExampleTest` |
| Raiz | 3 | `TestCase.php`, `CreatesApplication.php`, `DashboardTestCase.php` |

Arquivos (todos de dashboard, exceto `ExampleTest`):

```
v6/tests/Feature/DashboardAuthorizationTest.php
v6/tests/Feature/DashboardBaselineTest.php
v6/tests/Feature/DashboardConfigIntegrationTest.php
v6/tests/Feature/DashboardConteudosVisitadosWidgetTest.php
v6/tests/Feature/DashboardDiarioWidgetTest.php
v6/tests/Feature/DashboardEditModeTest.php
v6/tests/Feature/DashboardEnabledWidgetsTest.php
v6/tests/Feature/DashboardEnhancedToggleTest.php
v6/tests/Feature/DashboardEventosWidgetTest.php
v6/tests/Feature/DashboardFabTest.php
v6/tests/Feature/DashboardIndicatorsTest.php
v6/tests/Feature/DashboardLegislacoesWidgetTest.php
v6/tests/Feature/DashboardLicitacoesWidgetTest.php
v6/tests/Feature/DashboardModuleWidgetTest.php
v6/tests/Feature/DashboardNotAModuleTest.php
v6/tests/Feature/DashboardNoticiasWidgetTest.php
v6/tests/Feature/DashboardOuvidoriaWidgetTest.php
v6/tests/Feature/DashboardProfileLayoutTest.php
v6/tests/Feature/DashboardPublicacoesWidgetTest.php
v6/tests/Feature/DashboardResetLayoutTest.php
v6/tests/Feature/DashboardRevisoesWidgetTest.php
v6/tests/Feature/DashboardUsuariosWidgetTest.php
v6/tests/Feature/DashboardVisualParityTest.php
v6/tests/Feature/GaConfigurationTest.php
v6/tests/Feature/GaWidgetsTest.php
v6/tests/Feature/RevisoesWidgetTest.php
v6/tests/Feature/ExampleTest.php
v6/tests/Unit/DashboardGridModelTest.php
v6/tests/Unit/DashboardUnitTest.php
v6/tests/Unit/GoogleAnalyticsServiceTest.php
v6/tests/Unit/ExampleTest.php
```

### 1.2 Cobertura real

**Cobertura efetiva ≈ 0% da plataforma.** O que existe:

| Módulo | Testado? | Observação |
|---|---|---|
| Dashboard (16 widgets) | Sim | 24 arquivos; o único bloco realmente coberto |
| Configuração de GA | Sim | 2 arquivos |
| Notícias | Não | — |
| Diário Oficial | Não (apenas o widget do dashboard) | — |
| Transparência Fiscal | Não | — |
| Licitações / Contratos | Não | — |
| Ouvidoria | Não (apenas o widget) | — |
| LAI | Não | — |
| Formulários Dinâmicos | Não | — |
| Carta de Serviços | Não | — |
| CMS (CRUD, permissões) | Não | — |
| Portal público (rotas) | Não | — |
| API de dados abertos | Não | — |
| Layout / widgets / tags | Não | — |
| **Cobertura global** | **~2%** | Concentrada no dashboard (≈ 1.000 linhas de código) |

A v5 tem **apenas 5 arquivos** de teste (`v5/tests/`), sendo 2 `ExampleTest`
do skeleton e 1 `CnpjAlphanumericValidationTest`. **A v6 não regrediu: evoluiu
de 5 para 30 arquivos**, mas concentrou-se num único módulo.

### 1.3 Configuração

`v6/phpunit.xml`:

```xml
<testsuites>
    <testsuite name="Feature">
        <directory suffix="Test.php">./tests/Feature</directory>
    </testsuite>
    <testsuite name="Unit">
        <directory suffix="Test.php">./tests/Unit</directory>
    </testsuite>
</testsuites>
<php>
    <env name="APP_ENV" value="testing"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="QUEUE_DRIVER" value="sync"/>
</php>
```

**Problemas:**
1. **Não há `<testsuite name="E2E">`** — a suíte Playwright é totally separada.
2. `QUEUE_DRIVER=sync` — não exercita o caminho de fila.
3. `CACHE_DRIVER=array` — não exercita o cache real (Valkey/Redis).
4. **Não há banco de teste isolado** — os testes rodam contra `DB_DATABASE`
   do `.env`. Risco de contaminacao do banco de desenvolvimento.
5. `backupStaticAttributes="false"` é o padrão do PHPUnit 6 mas está obsoleto no PHPUnit 10+.
6. O `filter/whitelist` cobre `./app` — mas a v7 precisa cobrir `src/`.

**Comando:** `php artisan test` (ou `vendor/bin/phpunit`).

### 1.4 `v6/tests-e2e/` — Playwright + sniffCSS

Existe **apenas na v6** (a v5 não tem suíte E2E). É um projeto Node isolado.

**Tecnologia:** `@playwright/test ^1.49.1` + gate visual com `sniffCSS`
(`sniffCSS`, `sniffCSS-diff`, `sniffCSS-check` no `PATH`).

**Estrutura:**

```
v6/tests-e2e/
├── tests/
│   ├── auth.setup.ts           # login no CMS → .auth/cms.json (storageState)
│   ├── diario-portal.spec.ts   # histórias do portal público
│   ├── diario-cms.spec.ts      # histórias do CMS autenticado
│   └── support/manifest.ts     # registra checkpoints em sniff/manifest.json
├── histories/diario-oficial.md
├── scripts/
│   ├── sniff-baseline.sh       # captura os baselines visuais
│   └── sniff-visuals.sh        # gate visual + a11y (delta vs baseline)
├── sniff/{manifest.json,baselines/,current/}
├── fixtures/
├── playwright.config.ts
└── package.json
```

**Configuração (`playwright.config.ts`):**

- `testDir: './tests'`, `timeout: 120_000`, `expect.timeout: 15_000`.
- `fullyParallel: false`, `workers: 1` — **série**, porque as histórias
  compartilham o mesmo banco real do portal.
- `baseURL: process.env.BASE_URL ?? 'http://v6_diario_emergencial.local:32780'`.
- `viewport: 1440×900`, `ignoreHTTPSErrors: true`.
- Reporter: `list` + `html` (output em `reports/`).
- `screenshot: 'only-on-failure'`, `trace: 'retain-on-failure'`, `video: 'retain-on-failure'`.
- 3 projetos: `setup` (auth), `portal` (Desktop Chrome), `cms` (depende de `setup`, usa `storageState: '.auth/cms.json'`).

**Comandos (`v6/tests-e2e/package.json`):**

```bash
npm test            # playwright test
npm run test:portal # só o portal
npm run test:cms    # só o CMS autenticado
npm run auth        # apenas o setup de login
npm run baseline    # captura baselines visuais
npm run visuals     # gate: delta vs baseline + a11y (exit 1 se o front mudou)
npm run full        # test + visuals
npm run report      # playwright show-report
```

**Pré-requisitos** (do `README.md`): aplicação em execução via `BASE_URL`;
`sniffCSS` no `PATH`; `CMS_PASSWORD` exportada (**fail-closed** — sem senha o
teste de CMS não roda).

**O que cobre:** apenas o **Diário Oficial** (portal + CMS). É a **única**
cobertura E2E da plataforma — e ela é exemplar: combina teste funcional,
autenticação real, e **gate visual de regressão** (a ideia é valuable e deve
ser replicada).

---

## 2. Estratégia proposta para a v7

### 2.1 Ferramentas

| Camada | Ferramenta | Gate |
|---|---|---|
| Unitário + feature | **PHPUnit 13** (ou **Pest 4** sobre o PHPUnit 13) | Cobertura **≥ 90%** em `src/` (ADR-038), medida por **PCOV** |
| Análise estática | **PHPStan nível máximo** + Larastan | Sem relaxamento (ADR-040) |
| Formatação | **Laravel Pint** | `pint --test` no CI |
| Mutation testing | **Infection** | Obrigatório (ADR-039) |
| E2E | **Playwright** (reaproveitar o `v6/tests-e2e/`) | Módulos críticos verdes |
| Acessibilidade | axe-core / sniffCSS no E2E | WCAG 2.2 AA + eMAG (ADR-025) |
| Carga / smoke | `k6` ou `wrk` | Baseline de performance (ADR-014 → plano 14) |

A v7 já trae `phpunit/phpunit ^13.4`, `brianium/paratest ^7.27`,
`infection/infection ^0.35`, `phpstan/phpstan ^2.3` + `larastan ^3.10`,
`fakerphp/faker ^1.23`, `shipmonk/dead-code-detector` — ver
`public-v7/composer.json` e `cms/composer.json`.

### 2.2 Estrutura de testes por módulo

Seguindo o blueprint (plano 13):

```
src/modules/Publicacoes/Noticias/Test/
├── NoticiaPublicarActionTest.php      # unitário da Action
├── NoticiaFeatureTest.php             # feature (HTTP)
├── NoticiaApiTest.php                 # contrato do JSON
├── NoticiaPortalTest.php              # renderização do portal
└── NoticiaMigrationTest.php           # up/down da migration
```

**Regra:** um teste unitário que apenas espelha a implementação, sem valor de
regressão, **não conta** (ADR-038). E2E e feature/de não-regressão são
obrigatórios.

### 2.3 Matriz de casos de teste (Arrange / Act / Assert)

#### Autenticação e autorização (CMS)

| # | Arrange | Act | Assert |
|---|---|---|---|
| A1 | Usuário sem perfil, `GET /cms` | requête | 302 para `/login` |
| A2 | Usuário com perfil `noticias.ver` | `GET /cms/noticias` | 200 |
| A3 | Usuário **sem** `noticias.ver` | `GET /cms/noticias` | 403 (não 302, não 500) |
| A4 | Usuário sem `noticias.aprovar` | `POST /cms/noticias/{id}/publicar` | 403 |
| A5 | Usuário com `noticias.aprovar` | `POST /cms/noticias/{id}/publicar` | 302 + status = publicado |
| A6 | Usuário do grupo suporte | `GET /cms/suporte` | 200 (perfil reservado) |
| A7 | Módulo inativado | `GET /portal/{modulo}/…` | 404 |
| A8 | Módulo inativado | `GET /cms/{modulo}` | 404 (guard em **qualquer** área) |
| A9 | Login com senha errada | `POST /login` | 422/401 + **sem** revelar se o e-mail existe |
| A10 | Login válido | `POST /login` | 302 + cookie de sessão com `__Host-` + `SameSite=Lax` |

#### Layout / slots

| # | Arrange | Act | Assert |
|---|---|---|---|
| L1 | Preset `modelo1` selecionado | `GET /` | renderiza o fragmento do preset, com ordem determinística |
| L2 | Slot com 2 widgets à direita | render | posições 1 e 2 na pilha (sem posição absoluta) |
| L3 | Preferência de alto contraste ativa | render | a camada `a11y` vence **todas** as outras (ADR-016) |
| L4 | Widget de módulo A usado no slot do módulo B | render | renderiza, sem import entre módulos (ADR-013) |
| L5 | Slot removido | render | widget **não** aparece |
| L6 | Preset inexistente no banco | render | fallback determinístico (valor inválido é ignorado) |

#### Diário Oficial (PDF)

| # | Arrange | Act | Assert |
|---|---|---|---|
| D1 | Diário com 3 PDFs anexados | enfileira `GerarPdfDiarioJob` | 202 + `generation_status = enfileirado` |
| D2 | Job executado | worker | PDF em S3 + `generation_status = concluído` |
| D3 | PDF corrompido | job | falha **daquela página**, resto renderiza (resiliência por página) |
| D4 | Fila com 1 job que falha 3× | worker | vai para dead-letter, não trava a fila |
| D5 | Busca no PDF pelo portal | `GET /diario-oficial/viewer/{id}/search-pdf?q=` | 200 + JSON com páginas e offsets |

#### Ouvidoria / LAI (PII)

| # | Arrange | Act | Assert |
|---|---|---|---|
| O1 | Manifestação com CPF | grava | CPF criptografado; busca por CPF só via blind index |
| O2 | Busca por CPF exato | consulta | 1 registro; **log de auditoria** sem PII |
| O3 | Exportação da ouvidoria | exporta | **sem** PII em claro (máscara/omissão) |
| O4 | Pedido de exclusão (LGPD) | solicita | registro marcado para crypto-shredding |
| O5 | Portal do cidadão | acessa a manifestação | só a própria; as demais, 403 |

#### API de dados abertos

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | 10 notícias | `GET /api/v1/noticias` | JSON com as chaves `titulo`, `autor`, `fonte`, `data_publicacao`, `descricao`, `interacoes`, `slug`, `arquivos` |
| P2 | Repetir P1 | 2ª requisição | `ETag` + `304` |
| P3 | Requisição de fora da origem | `OPTIONS` | 204 + `Access-Control-Allow-Origin: *` |
| P4 | 61 requisições/min | 61ª | 429 (rate limit) |
| P5 | Dataset sem `updated_at` | serializa | 404 (versão do contrato mantida) |

#### Migrações

| # | Arrange | Act | Assert |
|---|---|---|---|
| M1 | Banco vazio | `migrate` | todas aplicadas, `migrate:status` sem pendência |
| M2 | Banco com dados | `migrate:fresh --seed` | recreate sem perda de dado de referência |
| M3 | Migration com `down()` | `migrate:rollback` | tabela renomeada para `backup_*`, **nada** removido |
| M4 | Migration com lock | 2ª sessão bloqueada | falha em 3 s (`lock_timeout`), sem lock indefinite |
| M5 | `migrate` 2× seguidas | 2ª execução | idempotente (sem erro) |

#### Regressão visual (E2E)

| # | Arrange | Act | Assert |
|---|---|---|---|
| E1 | Aplicação no ar + `CMS_PASSWORD` | `npm test` | portal + CMS verdes |
| E2 | Baseline capturado | `npm run visuals` | delta = 0 nos checkpoints |
| E3 | Mudança intencional de CSS | `npm run visuals` | delta > 0 → **exit 1** (falha) |
| E4 | Página do portal | axe-core | 0 violações WCAG 2.2 AA |

### 2.4 Fixtures e factories necessárias

| Factory | Tabela | Observação |
|---|---|---|
| `UserFactory` | `cms_users` | Argon2id; 1 perfil por usuário |
| `NoticiaFactory` | `noticias` (novo schema) | + `NoticiaCategoriaFactory` |
| `ConteudoFactory` | — | slug determinístico (`{slug}`), data no passado |
| `ObrigacaoFactory` | `ombudsman_requests` | CPF/CNPJ válido (check-digit) |
| `LicitacaoFactory` | `biddings` | `number` com e sem dígito (cobre o `orderByRaw` com regex) |
| `ContratoFactory` | `contracts` | `value` decimal, `proponent` PF e PJ |
| `DiarioFactory` | `diarios` | + `DiarioAreaFactory`, `DiarioSubAreaFactory` |
| `CartaServicoFactory` | `service_letters` | + `ServiceLetterSubjectFactory` (pivot) |
| `LayoutPresetFactory` | — | config de slots em JSONB |
| `MediaFactory` | `media` | imagem real em S3 (fixture binário) |

**Factories determinísticas:** sem `now()`/`rand()` soltos (ADR-001) — usar
`Clock` injetado e um `fake()`.

### 2.5 Fixtures de view / seed de ambiente de teste

- **Seeders são apenas demonstração** (ADR-041): vivem em
  `src/demonstration/{Module}/Seed.php`, **fora** das camadas de produção, e
  nunca são pré-requisito de teste.
- Os **dados de referência** (categorias, tipos, permissões) vêm de
  **migrations idempotentes** (ADR-017).
- Aplica-se a regra: `migrate` produz um banco com todos os dados de
  referência; `db:seed` só adiciona conteúdo de demonstração.

### 2.6 Como rodar (comandos exatos)

```bash
# Portal v7
cd public-v7
composer install
php artisan migrate --seed
php artisan test                      # suíte PHPUnit
vendor/bin/phpunit --coverage --min=90 # gate de cobertura
vendor/bin/pest --coverage            # se optar por Pest
vendor/bin/phpstan analyse --memory-limit=1G --no-progress
vendor/bin/pint --test
vendor/bin/infection --min-msi=80 --min-covered-msi=90

# E2E
cd public-v7
npm install
npx playwright install --with-deps
E2E=1 make check                       # gate único: lint + static + test + e2e
```

O comando único `make check` (já existe na v7) é o **gate estrutural**;
`make check` também aplica Pint/Prettier/ESLint antes de validar.

---

## 3. Migração dos testes existentes

| Teste v6 | Destino na v7 |
|---|---|
| `Dashboard*Test` (24 arquivos) | Reescrever como feature do módulo `Painel` (não é um módulo do `modulos` — é **estrutura**); a v7 já tem `DashboardWidgetInstance` e o widget registry |
| `GaConfigurationTest`, `GaWidgetsTest` | Módulo `Analytics` (ou serviço `Analytics`) — só se o GA for mantido; ADR-002 mantém o GA **opt-in e desligado por padrão** |
| `RevisoesWidgetTest` | Módulo `Revisoes` (workflow de revisão editorial) |
| `GoogleAnalyticsServiceTest` | Idem |
| `ExampleTest` (×2) | Descartar (placeholder do skeleton) |
| `v6/tests-e2e/*` | **Reaproveitar**: portar para `public-v7/tests-e2e/`, mantendo o gate `sniffCSS` como baseline visual por módulo |

**Regra de migração (ADR-042):** sem compatibilidade de código. Os testes do
legado **não** são portados — são **reescritos** a partir do comportamento
observável.

---

## 4. Referências

| Item | Caminho |
|---|---|
| Testes PHPUnit v6 | `v6/tests/` |
| Configuração PHPUnit | `v6/phpunit.xml` |
| Base de teste | `v6/tests/TestCase.php` |
| Base do dashboard | `v6/tests/DashboardTestCase.php` |
| Suíte E2E | `v6/tests-e2e/` |
| Configuração Playwright | `v6/tests-e2e/playwright.config.ts` |
| Scripts E2E | `v6/tests-e2e/package.json` |
| Documentação E2E | `v6/tests-e2e/README.md` |
| Baseline visual | `v6/tests-e2e/BASELINE.md` |
| Gate de cobertura (v7) | `public-v7/wiki/adr/ADR-038-cobertura-90-e-rigor-de-nao-regressao.md` |
| Mutation testing (v7) | `public-v7/wiki/adr/ADR-039-mutation-testing-obrigatorio.md` |
| Qualidade (v7) | `public-v7/wiki/adr/ADR-023-qualidade-phpstan-max-ratchet.md` |
| Acessibilidade (v7) | `public-v7/wiki/adr/ADR-025-acessibilidade-gate-ci.md` |
| Dependências de teste (v7) | `public-v7/composer.json`, `cms/composer.json` |
