# Legislação — Estratégia de Testes

> O legado **não tem testes** para este módulo: em `v6/tests/` não existe
> nenhuma suíte que cubra `LegislationController`, `ContentController`
> (case `legislacao`), `CategoryContentController` (case `legislacao`) nem
> `DatatableController@get_legislacao` (verificado por `grep -rln "Legislation"
> v6/tests/`, que não retorna nenhum arquivo). Existe apenas
> `v6/tests/Feature/DashboardLegislacoesWidgetTest.php`, que cobre o **widget
> do dashboard** e não o módulo em si. A estratégia abaixo é a proposta para
> a v7.

---

## 1. Estratégia

| Camada | Tipo | Foco |
|---|---|---|
| Feature (HTTP) | Pest/PHPUnit | Rotas de portal, CMS e API; regras de validação; autorização; redações. |
| Unit | Pest/PHPUnit | Normalização de texto, geração de slug, cálculo de descendentes, mapeamento de exportação. |
| Integração (DB) | Pest com `RefreshDatabase` | Comportamento das queries reescritas (filtro, ordenação, paginação). |
| Navegador (Dusk/Playwright) | E2E | Percurso real: filtro, paginação, exportação, reordenação de categorias. |

Comandos previstos na v7:

```bash
# suíte completa
php artisan test

# apenas o módulo
php artisan test --filter=Legislation

# navegador
php artisan dusk --filter=legislacao

# cobertura do módulo
php artisan test --coverage --min=80 --filter=Legislation
```

## 2. Fixtures e factories necessárias

```php
// (ilustrativo) database/factories/LegislationCategoryFactory.php
class LegislationCategoryFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => $this->faker->word(),
            'slug' => $this->faker->unique()->slug(),
            'order' => 1,
            'status' => true,
            'category_contents_id' => null,
        ];
    }

    public function legislation(): static
    {
        return $this->state(fn () => ['slug' => 'legislacao', 'name' => 'Legislação']);
    }
}
```

```php
// (ilustrativo) database/factories/LegislationFactory.php
class LegislationFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(4),
            'slug' => $this->faker->unique()->slug(),
            'text' => '<p>' . $this->faker->paragraph() . '</p>',
            'clicks' => 0,
            'status' => true,
            'galery' => false,
            'publication_date' => now()->subDays(10),
        ];
    }

    public function future(): static
    {
        return $this->state(fn () => ['publication_date' => now()->addMonth()]);
    }
}
```

Fixtures obrigatórias (semelhança com o banco atual):

| Fixture | Valor |
|---|---|
| Categoria raiz | `name = 'Legislação'`, `slug = 'legislacao'`, `category_contents_id = null`, `status = true` |
| Subcategoria A | `Constituição Federal` / `constituicao-federal`, pai = raiz |
| Subcategoria B | `Código Tributario` / `codigo-tributario`, pai = raiz |
| Subcategoria C | `Decretos` / `decretos`, pai = raiz |
| Netas | 1 subcategoria de nível 3 sob `Decretos` (para exercitar a recursão) |
| Normas | 25 normas em `Decretos` (≥ 2 páginas), 1 em cada outra categoria, 1 com `publication_date` futura, 1 com acento no título ("Legislação Orgânica") |
| Anexos | 1 `archive` com `view_pdf = true`, 1 com `highlight = null` |
| Usuário | papel com `legislacao.index/create/edit/delete` |

> Observação: o banco de produção inspecionado tem **0 conteúdos** de
> legislação — os testes **precisam** criar as fixtures, não podem depender de
> dados pré-existentes.

## 3. Casos de teste — Portal

### 3.1 `GET /legislacao` (rota `legislacao.index`)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | raiz + 3 subcategorias com `order` 3, 1, 2 | `GET /legislacao` | 302 → `/legislacao/decretos` (menor `order`) |
| P2 | sem subcategorias | `GET /legislacao` | erro controlado (não 500 com "Trying to get property of non-object") — **bug legado**: `$categoryFirst->slug` em null |
| P3 | cookie de sessão | `GET /legislacao` | sem CSRF e sem cookie de sessão indevido |

### 3.2 `GET /legislacao/{slug}` (rota `conteudo.legislacao.list`)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P4 | 3 normas em `decretos` | `GET /legislacao/decretos` | 200; view de lista renderizada; menu lateral com as categorias |
| P5 | slug inexistente | `GET /legislacao/inexistente` | 302 para `conteudo.legislacao.list` (comportamento de `getIndex`) |
| P6 | 1 norma futura + 1 passada | `GET /legislacao/decretos` | a futura **não** aparece na lista |
| P7 | 25 normas | `GET /legislacao/decretos` | paginação no servidor devolve a página correta (v7) |
| P8 | norma com título "Legislação Orgânica" | `GET /legislacao/decretos?q=legislacao` | encontra a norma (busca sem acento) |
| P9 | 2 layouts ativos (`modelo1`, `modelo3`) | `GET /legislacao/decretos` em ambos | 200; menu lateral presente nos dois formatos |
| P10 | `configuration 'layout' = 'default'` | `GET /legislacao/decretos` | 404 (regra do `getTemplateActived()`) — ou 200 se a v7 decidir tornar `default` alcançável (documentar a decisão) |

### 3.3 `GET /legislacao/{slug1}/{slug2}` (rota `conteudo.legislacao.view`)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P11 | norma com `clicks = 0` | `GET /legislacao/decretos/lei-x` | 200; `contents.clicks` passa a 1 |
| P12 | norma com `clicks = 7` | `GET /legislacao/decretos/lei-x` | `clicks` passa a 8 |
| P13 | norma **futura** | `GET /legislacao/decretos/lei-futura` | 302 para o índice; `clicks` **não** é incrementado |
| P14 | norma com anexo `view_pdf = true` | `GET /legislacao/decretos/lei-x` | 200; iframe do PDF presente |
| P15 | norma com `text` contendo `<script>` | `GET /legislacao/decretos/lei-x` | **conforme a política de sanitização escolhida** (o legado renderiza sem escape) |
| P16 | `content.status = false` | `GET /legislacao/decretos/lei-x` | **v7 deve negar** (o legado permite — bug) |

### 3.4 `POST /legislacao/filtro` (rota `conteudo.legislacao.fill`)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P17 | 5 normas em `decretos` | `POST /legislacao/filtro` `{slug: 'decretos'}` | JSON com 5 itens; cada um com `categoria = 'decretos'` |
| P18 | normas em 2 categorias | `POST` `{slug: 'decretos'}` | **só** as de `decretos` (o `where('category_contents_id', $subcategoria_id)` usa `pluck('id')` — cuidado com `array` vs `int`) |
| P19 | normas em 2023 e 2024 | `POST` `{slug:'decretos', data_inicial:'2024-01-01', data_final:'2024-12-31'}` | só as de 2024 — **valida o bug dos filtros sobrescritos** |
| P20 | idem | `POST` `{slug:'decretos', data_inicial:'2024-01-01'}` | só as ≥ 2024-01-01 |
| P21 | idem | `POST` `{slug:'decretos', data_final:'2024-12-31'}` | só as ≤ 2024-12-31 |
| P22 | norma "Lei Orgânica" | `POST` `{slug:'decretos', termo:'lei organica'}` | encontra a norma (normalização de acentos) — **v7** |
| P23 | idem | `POST` `{slug:'decretos', texto:'lei organica'}` | **também** encontra (o legado ignora `texto`) — **v7** |
| P24 | — | `POST` `{slug:'decretos', termo:"' OR 1=1 --"}` | **não** retorna tudo; resposta 200 com lista vazia (teste de regressão de injeção) |
| P25 | 500 normas | `POST` `{slug:'decretos'}` | a v7 deve paginar/limitar; o teste registra o limite definido |

### 3.5 `GET /legislacao/export` (rota `legislacao.export`, **só v6**)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P26 | 3 normas | `GET /legislacao/export?type=csv&sub_category=decretos` | 200; `Content-Type` CSV; cabeçalho `Título,Data de Publicação,Descrição,Atualizado em` |
| P27 | idem | `GET .../export?type=xls&sub_category=decretos` | 200; arquivo `.xlsx` |
| P28 | idem | `GET .../export?type=pdf&sub_category=decretos` | 200; PDF com 3 linhas |
| P29 | idem | `GET .../export?type=txt&sub_category=decretos` | 200; TSV |
| P30 | sub_category inválido | `GET .../export?sub_category=inexistente` | 404 |
| P31 | normas com `publication_date` futura | `GET .../export?sub_category=decretos` | a futura **não** é exportada (`publication_date <= now()`) |
| P32 | filtro de período | `GET .../export?sub_category=decretos&data_inicial=2024-01-01&data_final=2024-12-31` | exporta só o período |
| P33 | filtro de texto | `GET .../export?sub_category=decretos&termo=orgânica` | exporta só as correspondentes |

## 4. Casos de teste — CMS

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | usuário com `legislacao.index` | `GET /cms/conteudos/legislacao` | 200; view `prefecture.contents.legislacao.index` |
| C2 | usuário **sem** permissão | mesmo GET | redirect + flash "Você não tem autorização..." (middleware) |
| C3 | usuário anônimo | mesmo GET | redirect para `login` |
| C4 | 30 normas | `GET /cms/datatable/legislacao` | JSON Yajra com `data`, colunas `titulo`, `categoria`, `data_publicacao`, `categoria_slug`, `slug`, `id`, `edit`, `delete` |
| C5 | — | `GET /cms/conteudos/cadastrar/legislacao` | 200; select com as subcategorias (`categoryContentSelect`) |
| C6 | — | `POST /cms/conteudo` com `title`, `publication_date`, `category_contents_id`, `text` | 302; `contents.slug` gerado; `publication_date` convertido para `Y-m-d H:i:s` |
| C7 | título repetido | `POST /cms/conteudo` | slug único (sufixe numérico) |
| C8 | `title` com 121 chars | `POST /cms/conteudo` | 422/redirect com erro; nada criado |
| C9 | `text` vazio | `POST /cms/conteudo` | erro de validação |
| C10 | `publication_date` vazio | `POST /cms/conteudo` | erro de validação |
| C11 | update sem `slug` | `PUT /cms/conteudo/{id}` | erro de validação (slug é required no update) |
| C12 | update com slug inválido (`"a b"`) | `PUT /cms/conteudo/{id}` | redirect com erro "Link Permanente contém caracteres inválidos." |
| C13 | norma com `menu_items` | `DELETE /cms/conteudo/{id}` | norma removida **e** `menu_items` apontando para ela removidos |
| C14 | 3 subcategorias | `GET /cms/categorias/legislacao` | 200; ordenadas por `order` |
| C15 | nova categoria | `POST /cms/categories` JSON `{name, parent_id, order}` | 201; `slug` gerado; colisão prefixada com `cat<maxId>_` |
| C16 | reordenação | `POST /cms/ajax/ajust-categorias` `sortable_data=[3,1,2]` | `order` persistido conforme a nova ordem |
| C17 | edição de categoria | `GET /cms/categorias/editar/legislacao/{id}` | o `<select>` de pai **não** contém a própria categoria nem seus descendentes |
| C18 | `DELETE /cms/categories` JSON `{id}` | — | categoria removida recursivamente (filhos primeiro) |

## 5. Casos de teste — API de dados abertos

| # | Act | Assert |
|---|---|---|
| A1 | `GET /api/v1/legislacoes` | 200; envelope `data[]` com `titulo`, `data_publicacao`, `descricao`, `interacoes`, `slug`, `arquivos` |
| A2 | `?titulo=decreto` | só as normas com "decreto" no título |
| A3 | `?data_publicacao=01/01/2024` | só as publicadas em 01/01/2024 |
| A4 | `?data_publicacao=01/01/2024_30/04/2024` | só as do período |
| A5 | `?data_publicacao=99/99/9999` | 422 com `errors` |
| A6 | `?limite=2` | no máximo 2 itens |
| A7 | `?limite=` (vazio) | sem limite (comportamento atual: o `limit` só é aplicado se não for vazio) |
| A8 | `?descricao=...` | filtro por descrição funciona |

## 6. Testes de regressão dos bugs conhecidos

Estes testes devem **falhar** no legado e **passar** na v7:

| # | Bug | Teste |
|---|---|---|
| T1 | `$_GET['ano']` concatenado no SQL | `GET /legislacao/decretos?ano=2024%20OR%201=1--` → não retorna erro SQL nem conjunto inflado |
| T2 | `termo` não sanitizado | P24 |
| T3 | filtros de data sobrescritos | P19 |
| T4 | `texto` ignorado no filtro | P23 |
| T5 | view `legislacao` órfã | teste de arquitetura: nenhuma referência a `DB::table('legislacao')` no código |
| T6 | dupla paginação | teste de arquitetura/funcional: a resposta da listagem contém no máximo `per_page` itens |
| T7 | `categoryContents()` com chaves trocadas | unit: `children()` devolve apenas os filhos diretos |
| T8 | `getAllDescendants()` N+1 | teste de performance: número de queries constante com a profundidade da árvore |
| T9 | `clicks` lost update | 20 requisições simultâneas → `clicks` = 20 (com `increment()`) |
| T10 | `status = false` acessível | P16 |
| T11 | normafuturaincrementando `clicks` | P13 |
| T12 | `partials/link-row.blade.php` órfã | `php artisan view:cache` não deve falhar |

## 7. Seed de ambiente de teste

```php
// (ilustrativo) database/seeders/LegislationSeeder.php
class LegislationSeeder extends Seeder
{
    public function run(): void
    {
        $raiz = CategoryContent::firstOrCreate(
            ['slug' => 'legislacao'],
            ['name' => 'Legislação', 'status' => true, 'status_create' => 0]
        );

        $filhas = [
            ['name' => 'Constituição Federal', 'slug' => 'constituicao-federal', 'order' => 1],
            ['name' => 'Código Tributario',   'slug' => 'codigo-tributario',    'order' => 2],
            ['name' => 'Decretos',             'slug' => 'decretos',            'order' => 3],
        ];

        foreach ($filhas as $filha) {
            CategoryContent::firstOrCreate(
                ['slug' => $filha['slug']],
                ['name' => $filha['name'], 'order' => $filha['order'],
                 'status' => true, 'category_contents_id' => $raiz->id]
            );
        }

        // Normas de exemplo com slug determinístico
        Legislation::factory()->count(25)->create(['category_contents_id' => ...]);
    }
}
```

> O seeder deve ser **idempotente** e usar **slug** (nunca id fixo) para não
> depender da sequência `category_contents_id_seq`.

## 8. Teste já existente no legado (referência)

`v6/tests/Feature/DashboardLegislacoesWidgetTest.php` é o **único** teste do
repositório que toca a regra de contagem do módulo. O cabeçalho do arquivo
documenta a regra oficial:

```php
// $root   = CategoryContent::where('slug', 'legislacao')->first();
// $ids    = $root->getAllDescendants()->pluck('id');   // TODOS os descendentes
// Content::whereIn('category_contents_id', $ids)->count();
```

Ou seja: contam-se os conteúdos de **todos os descendentes** da raiz
`legislacao` (recursivo, incluindo netas), **excluindo a raiz**, **sem** filtro
de `status` nem de `publication_date`, e com soft-deleted fora (SoftDeletes).

O widget que consome essa regra é
`v6/app/Services/Dashboard/Widgets/LegislacoesWidget.php` (linhas 42-55).

> Esse é o único ponto do código que documenta explicitamente a semântica
> "todos os descendentes, raiz excluída". Preserve-a ao reescrever as queries.

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

1. `GET /legislacao` → 302 para a primeira subcategoria.
2. A lateral mostra a árvore de categorias com recursão (nível 3 visível).
3. O filtro por período **e** por texto funciona simultaneamente.
4. A lista é paginada no servidor (não há blocos de 10 no cliente).
5. A exportação respeita os mesmos filtros da listagem.
6. O contador `clicks` incrementa 1 por visita.
7. Norma futura não aparece nem é acessível.
8. `POST /api/v1/legislacoes` mantém o formato anterior.
9. No CMS: criar, editar, reordenar categoria, excluir norma — com as permissões
   `legislacao.*` respeitadas.
10. Nenhuma query ao `SELECT` na view `legislacao` no log de queries.
