# Legislação — Testes

> **Slug:** `Legislacao` · Suíte legada: PHPUnit do Laravel 5.5
> (baseline da plataforma: 650 testes / 2107 asserções / 9 skipped).
>
> Caminhos relativos a `lotep/`.

---

## 1. Estratégia

| Nível | Foco | Por quê |
|---|---|---|
| Unit | Models, escopos, accessors, seeder | Regras puras de slug, ordenação, `is_pdf`, URL e idempotência do seed. |
| Feature | HTTP real com middleware RBAC, storage fake e banco transacional | Garante que rotas, permissões, validação e efeitos colaterais (auditoria, arquivo físico) funcionam juntos. |

Práticas observadas:

- `use DatabaseTransactions` em toda a suíte; nada é truncado manualmente.
- `Storage::fake('public')` nos testes de anexo e portal.
- Helpers próprios para criar PDF com magic bytes válidos e usuário com
  `V2Role` + role Sentinel `operador`.
- CSRF desativado apenas nos testes de CMS (`withoutMiddleware(VerifyCsrfToken)`).
- Asserções sobre `v2_audit_logs` verificam `action` e `module`.

Total dedicado: **57 testes** (38 Feature + 19 Unit).

## 2. Casos — Unit

### 2.1 `V2LegislationModelTest` (5)

| Teste | Arrange | Act | Assert |
|---|---|---|---|
| `test_generates_uuid_and_slug_on_create` | `create(['title' => 'Lei de Teste Modelo'])` | persiste | id string de 36, slug `lei-de-teste-modelo`. |
| `test_updates_slug_when_title_changes` | item criado | `update(['title' => 'Novo Título'])` | slug regenerado. |
| `test_soft_delete_restricts_queries_and_restore` | item criado | `delete()` e `restore()` | `find` nulo, `withTrashed` presente, restauração volta. |
| `test_casts_boolean_and_datetime` | flags e data | leitura | `true` booleano e `Carbon`. |
| `test_search_scope_composes_with_published_and_category` | três itens (match, inativo, categoria errada) | `search()->published()->where(category)` | só o match. |

### 2.2 `V2LegislationScopesTest` (6)

| Teste | Cenário | Assert |
|---|---|---|
| `test_published_hides_future_and_inactive` | passado, futuro, inativo | só o passado aparece. |
| `test_from_date_filter` | duas datas | `whereDate >=` exclui o anterior. |
| `test_to_date_filter` | duas datas | `whereDate <=` exclui o posterior. |
| `test_keyword_searches_title_summary_and_number` | três alvos | cada termo encontra o registro certo. |
| `test_combined_filters_data_tipo_palavra_chave` | quatro registros | só a interseção. |
| `test_ordered_is_desc_by_publication_date` | três datas | ordem `newer, middle, older`. |

### 2.3 `V2LegislationFileTest` (5)

| Teste | Foco | Assert |
|---|---|---|
| `test_generates_uuid_and_casts` | UUID, `order`, `view_pdf` | 36 chars, int e bool. |
| `test_file_belongs_to_legislation_and_orders` | relação ordenada | ids na ordem `order asc`. |
| `test_is_pdf_detects_by_mime_or_extension` | três arquivos | PDF por MIME, por extensão; não-PDF falso. |
| `test_file_soft_delete` | `delete()` | some da query padrão, permanece em `withTrashed`. |
| `test_url_attribute_resolves_storage_path_and_keeps_absolute` | path relativo, URL absoluta, protocol-relative e vazio | `/storage/V2/assets/demo/edital.pdf`, URL intacta, `''`. |

### 2.4 `LegislationSeederTest` (3)

| Teste | Cenário | Assert |
|---|---|---|
| `test_run_sem_demo_cria_apenas_categorias` | `run(false)` | três categorias, nenhum documento demo. |
| `test_run_com_demo_cria_docs_ast_e_anexos` | `run(true)` | três documentos, `content` AST `doc`, dois anexos na portaria. |
| `test_run_eh_idempotente` | `run(true)` duas vezes | contagens estáveis. |

## 3. Casos — Feature CMS

### 3.1 `LegislationCmsControllerTest` (14)

| Teste | Arrange | Act | Assert |
|---|---|---|---|
| `test_index_returns_listing` | usuário `read`, um item | GET index | 200, título visível, "Limpar filtros". |
| `test_index_row_has_view_on_portal_button` | item com slug | GET index | link do portal no HTML. |
| `test_index_renders_full_import_component` | usuário `read` | GET index | ids dos modais, `LotepImporter`, rotas de import. |
| `test_store_creates_legislation_and_audit` | payload válido | POST store | 302, slug e data normalizados, auditoria `created`. |
| `test_store_creates_legislation_with_files_in_the_same_form` | dois PDFs no form | POST store | 2 anexos, path `legislacao/<id>/`, título/view_pdf/original_name. |
| `test_update_adds_files_via_main_form` | item existente | POST update com arquivo | 1 anexo novo. |
| `test_upload_file_creates_attachment_and_audits` | item + PDF válido | POST files | metadados completos e auditoria. |
| `test_upload_rejects_disallowed_file_type` | `.php` disfarçado | POST files | 302 com flash `error`, nenhum anexo. |
| `test_update_file_metadata` | anexo existente | POST files/{fileId} | título, descrição e `view_pdf` atualizados. |
| `test_delete_file_removes_storage_and_soft_deletes` | anexo com arquivo real | POST delete | arquivo físico some, linha soft-deleted. |
| `test_reorder_files` | dois anexos | POST files-reorder | JSON `success`, ordens trocadas. |
| `test_destroy_soft_deletes_document_and_files` | item + anexo | POST delete do item | ambos soft-deleted. |
| `test_create_form_links_to_category_manager` | usuário `write` | GET create | link para categorias. |
| `test_write_action_blocked_with_read_only_profile` | usuário `read` | POST store | 403/302 sem criar. |

### 3.2 `LegislationCategoryCmsTest` (7)

| Teste | Foco | Assert |
|---|---|---|
| `test_categories_page_lists_categories` | listagem | nome visível, link de volta. |
| `test_category_store_creates_and_audits` | criação | nome, ativo, ordem, descrição, auditoria. |
| `test_category_store_rejects_duplicate_name` | nome repetido | continua 1 registro com o nome. |
| `test_category_update_renames_and_refreshes_slug` | renomear | nome e slug novos, inativa, ordem 5. |
| `test_category_destroy_nulls_items_and_audits` | exclusão | categoria some, item sem categoria, slug com `-deleted-`, auditoria. |
| `test_recreate_category_after_delete_does_not_duplicate_slug` | recriar | novo id, slug original limpo é reutilizado. |
| `test_read_user_cannot_manage_categories` | permissão | nada é criado. |

### 3.3 `LegislationImportCmsTest` (12)

| Teste | Foco | Assert |
|---|---|---|
| `test_template_download_requires_write_and_returns_attachment` | template | 200 + `Content-Disposition` com nome `legislacao_template.xlsx`. |
| `test_template_download_blocked_for_read_only_profile` | permissão | 403. |
| `test_preview_validates_csv_and_returns_report` | CSV | 200 sem erros; `rows` presente. |
| `test_process_creates_item_with_category_and_audits` | linha com categoria existente | item criado, data `00:00:00`, auditoria. |
| `test_process_creates_missing_category_and_persists` | categoria nova | categoria criada e vinculada. |
| `test_process_creates_category_when_slug_preserved_in_trashed_row` | slug de categoria apagada | novo id, slug diferente. |
| `test_process_orders_new_categories_incrementally_after_last` | duas categorias novas | `max(order)+1` e `+2`, existente intacta. |
| `test_process_updates_existing_by_number_and_year` | `number=333, year=2024` | 1 registro, campos atualizados. |
| `test_preview_accepts_excel_csv_file` | extensão `.xlsx` com CSV interno | aceito pelo motor. |
| `test_preview_rejects_file_above_20mb_with_friendly_error` | 21 MB | 422 com mensagem amigável. |
| `test_preview_accepts_files_between_5mb_and_20mb` | 7 MB | validador passa. |
| `test_read_user_cannot_preview_import` | permissão | 302/403, nenhuma criação. |

## 4. Casos — Feature Portal

`LegislationPortalTest` (5):

| Teste | Arrange | Act | Assert |
|---|---|---|---|
| `test_download_serves_storage_file_as_attachment` | item publicado + arquivo no fake | GET download | 200, `application/pdf`, `attachment`, `BinaryFileResponse`. |
| `test_download_redirects_external_url` | `file_url` https | GET download | 302 para a URL externa. |
| `test_download_blocked_for_unpublished_legislation` | item inativo | GET download | 404. |
| `test_index_filter_bar_has_clear_button` | item publicado | GET index | "Limpar", rota e título. |
| `test_show_renders_download_for_non_pdf_and_view_for_pdf` | PDF com `view_pdf`, não-PDF | GET show | rota de download para o não-PDF, URL direta e badge para o PDF. |

## 5. Fixtures e factories necessárias

| Recurso | Como é criado hoje |
|---|---|
| Usuário CMS | `App\Admin\User` + `V2Role` (`permissions_v2`) + role Sentinel `operador` + `Activation::complete` + `Sentinel::login`. |
| PDF válido | `tempnam()` + conteúdo `%PDF-1.4` seguido de `%%EOF`, `UploadedFile` com `test=true`. |
| Planilha | `makeXlsxFile($header, $row, $padBytes, $name)` gera CSV com extensão de Excel. |
| Categoria | `V2LegislationCategory::create()` direto. |
| Norma | `V2Legislation::create(['title', 'publication_date', 'is_active'])`. |
| Anexo | `V2LegislationFile::create()`. |
| Storage | `Storage::fake('public')` no `setUp`. |

Não há factories de model no legado; os testes instanciam models diretamente.
No V7, criar factories determinísticas com `fake()->unique()` e relógio fixo.

## 6. Como rodar

```bash
cd /home/one/projects/maxima/v7/lotep

# suíte inteira no container (equivale ao alvo `test` do Makefile do lotep)
docker compose exec -T php bash -c 'cd /var/www/html && vendor/bin/phpunit --no-coverage'

# suíte do módulo
docker compose exec -T php vendor/bin/phpunit tests/Feature/V2/Modules/Publicacoes/Legislacao
docker compose exec -T php vendor/bin/phpunit tests/Unit/V2/Modules/Publicacoes/Legislacao

# um arquivo
docker compose exec -T php vendor/bin/phpunit tests/Feature/V2/Modules/Publicacoes/Legislacao/LegislationPortalTest.php

# suíte de importação legada (também grava nas tabelas do módulo)
docker compose exec -T php vendor/bin/phpunit tests/Unit/V2/Modules/Infraestrutura/MigracaoLegada/LegislacaoLegacyImporterTest.php
```

O projeto usa PHPUnit 6 (`composer.json`) e não possui o comando `artisan test`
do Laravel moderno; o caminho canônico é `vendor/bin/phpunit` dentro do
container (`Makefile:95-96`).

Observação: o caminho do teste do importador legado é
`tests/Unit/V2/Modules/Infraestrutura/MigracaoLegada/LegislacaoLegacyImporterTest.php`,
embora a classe viva em `V2/Modules/Support/MigracaoLegada`.

## 7. Lacunas de cobertura

| Área sem teste | Risco |
|---|---|
| Agendamento no portal (item futuro não listado) | Coberto indiretamente por `published`, mas sem feature de HTTP. |
| Download de item com `legislation_id = NULL` | Sem teste; comportamento 404 não é intencional documentado. |
| Colisão de slug no `store`/`update` manual | Sem teste; erro de banco vira 500. |
| Magic bytes de arquivo não-PDF | Testa apenas a rejeição por MIME (`.php`), não a assinatura inválida. |
| HTML/AST malicioso no portal | Sem teste de XSS. |
| Reordenação com id de outra norma | Sem teste; o escopo por `legislation_id` deveria impedir. |
| Importação concorrente | Sem teste; não há unique no banco. |
| SEO/canonical | Sem teste. |

## 8. Estratégia para o V7

1. Testes de unidade puros para o domínio da norma (publicação, versão,
   consolidação e slug estável), com relógio injetado.
2. Feature tests por ação do CMS, verificando 403 por token
   (`legislacao.aprovar`, `legislacao.arquivar`) e não por perfil.
3. Teste de contrato do renderer: AST com nós proibidos não deve produzir HTML
   executável.
4. Teste de migração de dados: importar fixture com `content` legado (JSON
   serializado) e conferir versão inicial, anexos e redirecionamentos.
5. Teste de exportação de dados abertos com schema estável e `ETag`.
6. Gate de cobertura de 90% e mutation testing conforme ADR-038/ADR-039.
