# Órgãos — Estratégia de Testes

> Escopo: como validar o módulo Órgãos no legado (`v6`) e como cobrir a reconstrução moderna.
> Referência da infraestrutura de testes: `v6/phpunit.xml`, `v6/tests/TestCase.php`,
> `v6/tests/CreatesApplication.php`, `v6/tests/DashboardTestCase.php`.

---

## 1. Infraestrutura existente

| Item | Valor |
|---|---|
| Runner | PHPUnit `~6.0` (`v6/composer.json`, `require-dev`) |
| Configuração | `v6/phpunit.xml` — suítes `Feature` (`./tests/Feature`) e `Unit` (`./tests/Unit`), `suffix="Test.php"` |
| Base | `Tests\TestCase` (`v6/tests/TestCase.php`) + trait `Tests\CreatesApplication` (`v6/tests/CreatesApplication.php`) |
| Ambiente | `.env` (não há `.env.testing` no repositório) |
| Transações | `Illuminate\Foundation\Testing\DatabaseTransactions` (usado em `v6/tests/DashboardTestCase.php`) |
| Cache | `CACHE_DRIVER=array`, `SESSION_DRIVER=array`, `QUEUE_DRIVER=sync` (definidos em `v6/phpunit.xml`) |
| Helpers | `v6/tests/DashboardTestCase.php` carrega `app/Library/Helper/appHelpers.php` via `require_once` (o composer não autoloja esse arquivo) |

> O módulo Órgãos **não tem** testes no legado. Todos os testes existentes (`v6/tests/Feature/`,
> `v6/tests/Unit/`) cobrem o Dashboard. O que segue é a suíte proposta.

## 2. Estratégia

| Camada | Foco | Ferramenta |
|---|---|---|
| **Unit** | Regras puras: montagem de URL do menu, filtro de busca da API, ordenação, cálculo de `clicks` | PHPUnit + `RefreshDatabase`/`DatabaseTransactions` |
| **Feature (HTTP)** | Rotas do CMS, do portal e da API; status, payload e efeitos colaterais | `Laravel\Http\Client` / `MakesHttpRequests` |
| **Feature (browser)** | Grade DataTable, botões condicionais, exclusão em lote | Laravel Dusk ou Playwright (a reconstruir) |
| **Contrato (API)** | Forma do JSON `data[]` e dos campos | Teste dedicado de contrato |

**Princípio de equivalência:** cada teste abaixo afirma o **comportamento observável** described no
capítulo 00, para garantir que a reconstrução em Laravel 11/12 preserve o mesmo contrato.

## 3. Fixtures e factories necessárias

### 3.1 Facts (estado mínimo do banco)

```php
// (ilustrativo) database/factories/OrganFactory.php
namespace Database\Factories;

use App\Prefecture\Content;
use Illuminate\Database\Eloquent\Factories\Factory;

class OrganFactory extends Factory
{
    public function definition()
    {
        return [
            'title' => $this->faker->company(),
            'slug' => $this->faker->slug(),
            'text' => $this->faker->paragraph(),
            'category_contents_id' => 32,
            'status' => true,
            'clicks' => 0,
            'galery' => false,
        ];
    }
}
```

| Fixture | Valor | Origem |
|---|---|---|
| `category_contents` id 32 | `name = 'Órgãos'`, `slug = 'orgaos'`, `category_contents_id = NULL` | `v6/database/seeds/CategoryContentTableSeeder.php:180-185` |
| `modulos` id 17 | `slug = 'orgaos'`, `status = false` | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:124-129` |
| `resources.slug = 'orgaos'` | `modulo_id = 17` | `v6/database/seeds/ResourcesTableSeeder.php:108-115` |
| `menu_orders` | `slug = 'orgaos'`, `modulo_id = 17` | `v6/database/seeds/MenuOrderSeeder.php:87` |
| `configurations.key = 'layout'` | `modelo1`..`modelo6` (o valor precisa ter `portal.layouts.<layout>.home.index`) | `v6/app/Http/Traits/GetInformationsPrefectureTrait.php:92-102` |
| `configurations.key = 'noticias'` | `'0'` (habilita a lista de notícias do órgão) | `v6/app/Helpers/functions.php:594-604` |
| `links` | registro usado pelo item agrupador "Órgãos" (`links_id`) | `v6/database/seeds/MenuItemsTableSeeder.php:196-208` |
| `menu_items` | item agrupador (`type = 0`) + item filho (`type = 2`, `contents_id` = órgão) | idem |

### 3.2 Setup mínimo do CMS

```php
// (ilustrativo) tests/Feature/OrgaosTestCase.php
namespace Tests\Feature;

use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;

abstract class OrgaosTestCase extends TestCase
{
    use DatabaseTransactions;

    protected function setUp()
    {
        parent::setUp();

        require_once __DIR__ . '/../app/Library/Helper/appHelpers.php';
        $this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
    }

    protected function loginAsAdmin()
    {
        // Papel 'administrador' com as 8 permissões orgaos.* (v6/database/seeds/RolesTableSeeder.php:52)
    }
}
```

> O `require_once` do `appHelpers.php` é **necessário**: `has_access()` é uma função global não
> autoloadada pelo composer e é usada dentro das views do CMS e do `struct_views()`.

## 4. Casos de teste — API (público)

> Contrato e recursos da API: `09-API-E-DADOS-ABERTOS.md`.

| # | Arrange | Act | Assert |
|---|---|---|---|
| A1 | 2 órgãos na categoria 32 | `GET /api/v1/orgaos` | 200; `data` com 2 itens; cada item com `titulo`, `descricao`, `interacoes`, `slug`, `arquivos` |
| A2 | 2 órgãos | `GET /api/v1/orgaos?limite=1` | `count(data) === 1` (o `limite` **é** aplicado) |
| A3 | Órgão "PROCON" e órgão "Gabinete" | `GET /api/v1/orgaos?titulo=PROCON` | **Comportamento atual (bug):** retorna os 2 órgãos. O teste deve **documentar o bug** e, na versão moderna, afirmar `count(data) === 1` |
| A4 | Órgão com `text` HTML | `GET /api/v1/orgaos` | `descricao` não contém tags HTML (`strip_tags`) |
| A5 | Órgão com `text` vazio | `GET /api/v1/orgaos` | `descricao` ausente do payload (`when($description != "", ...)`) |
| A6 | Órgão com `clicks = 7` | `GET /api/v1/orgaos` | `interacoes === 7` |
| A7 | Órgão com `archives` | `GET /api/v1/orgaos` | `arquivos` é array de `{titulo, descricao, tipo, url}` |
| A8 | Conteúdo de outra categoria (ex.: notícia) | `GET /api/v1/orgaos` | Notícia **não** aparece no payload |
| A9 | `titulo` com acento (`Procuradoria`) | `GET /api/v1/orgaos?titulo=procuradoria` | Idem A3 (bug) / na versão moderna, deve encontrar |
| A10 | Sem órgãos | `GET /api/v1/orgaos` | 200 com `data: []` |

## 5. Casos de teste — Portal (público)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | Órgão "PROCON" com `text` preenchido; `layout = modelo1` | `GET /a-instituicao/orgaos/procon` | 200; título visível; texto visível |
| P2 | Órgão sem `text` | `GET /a-instituicao/orgaos/procon` | 200; exibe o bloco "CONTEÚDO NÃO ENCONTRADO" do componente |
| P3 | Órgão inexistente | `GET /a-instituicao/orgaos/nao-existe` | Comportamento legado: `getContentDataWith` devolve `null` e `$contentNew->title` dispara erro de `null`; **corrigir** para 404 na versão moderna |
| P4 | Órgão com `clicks = 5` | `GET /a-instituicao/orgaos/procon` | `contents.clicks` passa a 6 (efeito colateral de `updateClicks`) |
| P5 | Órgão com 2 notícias vinculadas (`orgaos_id`) | `GET /a-instituicao/orgaos/procon` | ambas as notícias aparecem na lista "Notícias de PROCON" |
| P6 | Notícia vinculada com `publication_date` futura | `GET /a-instituicao/orgaos/procon` | a notícia **não** aparece (filtro `publication_date <= now`) |
| P7 | Órgão com 2 eventos (`calendar_events.orgaos_id`, `status = 1`, `final_date >= hoje`) | `GET /a-instituicao/orgaos/procon` | bloco "Agenda de eventos" com os 2 eventos |
| P8 | Órgão com 7 eventos válidos | `GET /a-instituicao/orgaos/procon` | apenas **5** eventos exibidos (limite no SQL) |
| P9 | Evento com `status = 0` ou `final_date` passada | `GET /a-instituicao/orgaos/procon` | evento não exibido |
| P10 | `modulos.status` de `noticias` = `false` | `GET /a-instituicao/orgaos/procon` | o bloco "Notícias de ..." **não** é renderizado |
| P11 | `configurations.key = 'noticias'` != `'0'` | `GET /a-instituicao/orgaos/procon` | exibe a imagem de aviso no lugar da lista |
| P12 | Órgão com `galery = 1` e 2 imagens em `archives` | `GET /a-instituicao/orgaos/procon` | galeria renderizada com 2 itens |
| P13 | `configurations.key = 'layout'` = `modelo2` | `GET /a-instituicao/orgaos/procon` | 200 (o `modelo2` inclui o `orgaosNews` do `modelo1`) |
| P14 | Órgão com `linksUnique.link` (vídeo) | `GET /a-instituicao/orgaos/procon` | player de vídeo renderizado |
| P15 | Órgão com item de menu (`menu_items.contents_id`) | renderizar o menu principal | o link gerado é `/a-instituicao/orgaos/<slug>` |

## 6. Casos de teste — CMS

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | Usuário com `orgaos.index` | `GET /cms/conteudos/orgaos` | 200; view `prefecture.contents.orgaos.index` |
| C2 | Usuário **sem** `orgaos.index` | `GET /cms/conteudos/orgaos` | redirect + flash "Você não tem autorização..." (comportamento do `CheckRoutePermission`) |
| C3 | Usuário com `orgaos.create` | `GET /cms/conteudos/orgaos` | o botão "Cadastrar Órgão" aparece no HTML |
| C4 | Usuário sem `orgaos.delete` | `GET /cms/conteudos/orgaos` | o checkbox de lote e o botão de exclusão em lote **não** aparecem |
| C5 | Usuário com `orgaos.index` | `GET /cms/datatable/orgaos` | JSON com `value`, `titulo`, `id`, `galeria`, `slug`, `edit`, `delete`, `active`, `inactivate` |
| C6 | Usuário sem `orgaos.edit` | `GET /cms/datatable/orgaos` | `edit === false` para todas as linhas |
| C7 | Usuário sem `orgaos.delete` | `GET /cms/datatable/orgaos` | `delete === false` para todas as linhas |
| C8 | Usuário com `orgaos.create` | `POST /cms/conteudo` com `title`, `text`, `category_contents_id = 32` | 302; nova linha em `contents` com `category_contents_id = 32`; `slug` gerado a partir do título |
| C9 | Órgão existente com o mesmo `title` | `POST /cms/conteudo` | `slug` recebe o sufixo `-<maxid+1>` (colisão resolvida) |
| C10 | `title` com 121 caracteres | `POST /cms/conteudo` | 422/redirect com erro em `title` (`max:120`) |
| C11 | `text` vazio | `POST /cms/conteudo` | erro em `text` (`required`) |
| C12 | `archive_highlight` com 6 MB | `POST /cms/conteudo` | erro de validação (`max:20000`) |
| C13 | `archive_highlight` com `.exe` | `POST /cms/conteudo` | erro de validação (`mimes`) |
| C14 | Órgão com `menu_items` vinculado | `DELETE /cms/conteudo/delete/{id}` | `menu_items` removidos; `contents.deleted_at` preenchido; flash informativo |
| C15 | Órgão com `menu_items` vinculado | `DELETE /cms/conteudo/{id}` (`destroy`) | idem |
| C16 | 2 órgãos | `POST /cms/conteudo/deleteAll` com `toDeleteAll[]` | ambos removidos (soft delete) |
| C17 | Órgão com `galery = 1` | `GET /cms/conteudos/editar/orgaos/{id}` | a aba "Galeria" aparece no formulário |
| C18 | `modulos.status` de `orgaos` = `false` | renderizar o menu lateral do CMS | a entrada "Órgãos" **não** aparece |
| C19 | `modulos.status` de `orgaos` = `true` + `orgaos.index` | renderizar o menu lateral do CMS | a entrada "Órgãos" aparece, apontando para `/cms/conteudos/orgaos` |
| C20 | `modulos.status` de `orgaos` = `true` sem `orgaos.index` | renderizar o menu lateral do CMS | a entrada **não** aparece |

## 7. Casos de teste — vínculo com Notícias e Agenda

| # | Arrange | Act | Assert |
|---|---|---|---|
| N1 | `modulos.status` de `orgaos` = `false` | `GET /cms/conteudos/cadastrar/noticias` | o select "Órgãos" **não** aparece (cadeza causal: `modulos.status` -> `ModulosServiceProvider` -> `$modulos` -> `prefecture.contents.noticias.form`) |
| N2 | `modulos.status` de `orgaos` = `true` | `GET /cms/conteudos/cadastrar/noticias` | o select "Órgãos" aparece, populado com os órgãos (`title as name`, `id as valor`) |
| N3 | `modulos.status` de `orgaos` = `true` | `POST /cms/conteudo` (notícia) com `orgaos = <id do órgão>` | `contents.orgaos_id` gravado |
| N4 | `modulos.status` de `orgaos` = `true` | `POST /cms/conteudo` com `orgaos = "0"` | `contents.orgaos_id` gravado como NULL (`$request->orgaos != "0" ? ... : null`) |
| N5 | Notícia com `orgaos_id` válido | `GET /a-instituicao/orgaos/procon` | a notícia aparece na lista do órgão |
| N6 | `Cache::rememberForever('modulos')` preenchido; `modulos.status` alterado direto no banco | renderizar a view de notícias | o valor em cache ainda vale (cache não invalidado) — comportamento **esperado** no legado, a ser corrigido com invalidação explícita |

## 8. Testes de unidade

| # | Alvo | Asserção |
|---|---|---|
| U1 | `MenuItemPresenter::getLink()` com `categoryContent->id = 32` e `content->slug = 'procon'` | retorna `.../a-instituicao/orgaos/procon` |
| U2 | `MenuItemPresenter::getShortLink()` com `categoryContent->id = 32` | retorna `a-instituicao/orgaos/procon` |
| U3 | `ContentController::generateSlug('PROCON')` | retorna `procon` |
| U4 | `Swagger\V1\Portal\Controller::caseInsensitiveAccentFreeSearch('Procuradoria', 'title')` | gera `lower(TRANSLATE(...)) like '%procuradoria%'` |
| U5 | `Content::orgaos()` | retorna somente os conteúdos com `orgaos_id` = id do órgão |
| U6 | `Content::eventosOrgaos()` | retorna somente os eventos com `orgaos_id` = id do órgão |
| U7 | `Content::scopeBySlug()` | filtra por `slug` |
| U8 | `DatatableController::struct_views()` com `has_access` falso | injeta `edit = false`, `delete = false`, `active = false`, `inactivate = false` |

## 9. Testes de contrato da API (recomendado)

Congelar o payload para evitar quebra de consumidores:

```php
// (ilustrativo) tests/Feature/OrgaosApiContractTest.php
public function testPayloadKeysAreStable()
{
    Organ::factory()->create(['title' => 'PROCON', 'clicks' => 3]);

    $response = $this->getJson('/api/v1/orgaos');

    $response->assertStatus(200)
        ->assertJsonStructure([
            'data' => [
                [
                    'titulo', 'descricao', 'interacoes', 'slug',
                    'arquivos' => [['titulo', 'descricao', 'tipo', 'url']],
                ],
            ],
        ]);
}
```

> Ao migrar para Laravel moderno, este teste deve passar **antes e depois** da migração — é o guardião
> de equivalência funcional da API.

## 10. Testes de desempenho (regressão)

| # | Cenário | Métrica | Alvo (legado) | Alvo (moderno) |
|---|---|---|---|---|
| D1 | `GET /cms/datatable/orgaos` com 10.000 conteúdos | número de linhas materializadas | 10.000 (`DB::table()->get()`) | <= pageSize (25) |
| D2 | `GET /a-instituicao/orgaos/procon` com 500 notícias vinculadas | linhas lidas | 500 | <= 10 (paginação) |
| D3 | `GET /api/v1/orgaos` sem `limite` com 1.000 órgãos | linhas lidas | 1.000 | default (ex.: 100) documentado |
| D4 | `GET /cms/conteudos/cadastrar/noticias` com 500 órgãos | linhas lidas | 500 | <= 500 (necessário para o select) ou busca assíncrona |

## 11. Como rodar

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

# Apenas uma suíte
./vendor/bin/phpunit --testsuite Feature
./vendor/bin/phpunit --testsuite Unit

# Arquivo específico
./vendor/bin/phpunit tests/Feature/OrgaosApiContractTest.php

# Filtrar por nome de teste
./vendor/bin/phpunit --filter testPayloadKeysAreStable

# Com relatório de cobertura (xdebug)
./vendor/bin/phpunit --coverage-html coverage
```

| Comando | Efeito |
|---|---|
| `php artisan migrate --env=testing` | Aplica as migrations (incluindo a criação da view `a_orgaos`) |
| `php artisan db:seed --class=CategoryContentTableSeeder` | Garante a categoria 32 |
| `php artisan db:seed --class=ResourcesTableSeeder` | Garante o recurso `orgaos` |
| `php artisan db:seed --class=RolesTableSeeder` | Garante as permissões `orgaos.*` |
| `php artisan cache:clear` | Limpa o cache `modulos` (indispensável após mexer em `modulos.status`) |

> **Requisito funcional:** o banco de teste precisa ter a view `public.a_orgaos` criada — ela vem das
> migrations `2023_04_10_145838_create_view_to_orgaos.php` e `2024_11_29_093120_update_view_orgaos.php`.
> Sem elas, `GET /cms/datatable/orgaos` falha com "relation a_orgaos does not exist".

## 12. Checklist de equivalência (para o aceite da v7)

- [ ] `GET /cms/conteudos/orgaos` responde 200 para usuário com `orgaos.index` e 302 para usuário sem.
- [ ] `GET /cms/datatable/orgaos` devolve as mesmas chaves (`value`, `titulo`, `id`, `galeria`, `slug`, `edit`, `delete`, `active`, `inactivate`).
- [ ] `POST /cms/conteudo` com `category_contents_id = 32` cria um órgão com `slug` derivado do título.
- [ ] `DELETE` de um órgão remove os `menu_items` vinculados.
- [ ] `GET /a-instituicao/orgaos/{slug}` renderiza título, texto, galeria, vídeo, agenda (máx. 5) e notícias vinculadas.
- [ ] `GET /api/v1/orgaos` mantém o envelope `data` e as chaves `titulo`, `descricao`, `interacoes`, `slug`, `arquivos`.
- [ ] O link do menu do portal continua sendo `/a-instituicao/orgaos/{slug}`.
- [ ] O `clicks` do órgão é incrementado a cada visita.
- [ ] `modulos.status` continua controlando a visibilidade do select "Órgãos" no formulário de notícias.
