# A Instituição — Estratégia de Testes

> Base de referência: a suíte existente em `v6/tests/` (PHPUnit ~6.0,
> `v6/phpunit.xml`, `v6/tests/TestCase.php`).
> Este arquivo descreve a cobertura **necessária** para o módulo A Instituição —
> o legado **não possui** nenhum teste para ele (verificado: a suíte da v6 cobre
> apenas Dashboard, GA e Widgets).

---

## 1. Estado atual da suíte

| Item | Valor |
|---|---|
| Framework | PHPUnit `~6.0` (`v6/composer.json:50`) |
| Configuração | `v6/phpunit.xml` — suítes `Feature` (`./tests/Feature`) e `Unit` (`./tests/Unit`) |
| Bootstrap | `v6/tests/TestCase.php` (usa `CreatesApplication`) |
| Base de dados | `DB_CONNECTION` do `.env` de teste; `DatabaseTransactions` disponível |
| Auth | `cartalyst/sentinel` — o helper `v6/tests/DashboardTestCase.php` já sabe criar role e usuário e autenticar pela rota de login |
| Cache/Session/Queue | `array` / `array` / `sync` (definidos em `v6/phpunit.xml`) |
| Testes existentes do módulo | **nenhum** |

Helpers reaproveitáveis de `v6/tests/DashboardTestCase.php`:

| Helper | Uso |
|---|---|
| `createRole($slug, array $permissions)` | Cria uma role Sentinel com permissões específicas. |
| `createUser($roleSlug, array $permissions)` | Cria e autentica (via rota de login) um usuário Sentinel. |
| `setModules(array $statuses)` | Liga/desliga `modulos.status` dentro da transação do teste. |
| `require_once app/Library/Helper/appHelpers.php` | Carrega `has_access()` — helper **não** autoloadado pelo Composer. |

> Atenção: no PHPUnit 6, `setUp()` **não** aceita `: void` (a assinatura é
> `protected function setUp()`), conforme o padrão já usado nos testes existentes.

## 2. Estratégia

| Tipo | Escopo | Quantidade-alvo |
|---|---|---|
| **Feature (portal)** | Renderização da página pública, contagem de cliques, 404, roteamento legado | 8 |
| **Feature (CMS)** | Permissões, validação, CRUD, exclusão em lote, limpeza de `menu_items` | 12 |
| **Unit** | Geração de slug, colisão, escolha de view por categoria, regras do presenter | 6 |
| **Regressão de bug** | Um teste por bug documentado em `08-GAPS-E-DIVIDAS.md` | 5 |

Nomenclatura: `tests/Feature/Instituicao/…Test.php` e
`tests/Unit/Instituicao/…Test.php`.

## 3. Fixtures necessárias

### 3.1 Factory

```php
// exemplo ilustrativo
class InstitutionalContentFactory extends Factory
{
    protected $model = Content::class;

    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(4),
            'text' => $this->faker->paragraphs(3, true),
            'slug' => $this->faker->unique()->slug(2),
            'category_contents_id' => 6,
            'galery' => false,
            'clicks' => 0,
            'publication_date' => now(),
        ];
    }

    public function comGaleria(): self
    {
        return $this->state(fn () => ['galery' => true]);
    }
}
```

### 3.2 Seeder de ambiente de teste

```php
// exemplo ilustrativo
class InstituicaoTestSeeder extends Seeder
{
    public function run()
    {
        // categoria raiz do módulo
        $categoria = CategoryContent::updateOrCreate(
            ['slug' => 'a-instituicao'],
            ['name' => 'A Instituição', 'status_create' => 1]
        );

        // módulo habilitado no menu do CMS
        Modulos::updateOrCreate(['slug' => 'a-instituicao'], ['name' => 'A Instituição', 'status' => true]);

        // layout do portal
        Configuration::updateOrCreate(['key' => 'layout'], ['value' => 'modelo1']);

        // 5 conteúdos institucionais
        foreach (['Prefeito', 'Vice-prefeito', 'Aspectos Gerais', 'Localização', 'Sobre'] as $titulo) {
            Content::updateOrCreate(
                ['slug' => \Illuminate\Support\Str::slug($titulo), 'category_contents_id' => $categoria->id],
                ['title' => $titulo, 'text' => 'Conteúdo de teste.', 'clicks' => 0]
            );
        }
    }
}
```

### 3.3 Pré-condições por teste

| Teste | Pré-condição |
|---|---|
| Portal | `configurations.layout = 'modelo1'`; `modulos.status` de `a-instituicao` = true. |
| Permissões | Role com o subconjunto de permissões `a-instituicao.*` desejado. |
| Módulo desligado | `modulos.status = false` para `a-instituicao`. |
| View SQL | A view `a_instituicao` deve existir no banco de teste — ou o teste deve ser reescrito para usar a query Eloquent (recomendado na v7). |

## 4. Casos de teste — Portal público

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P-1 | Página institucional renderiza | 1 conteúdo na categoria 6 com `text` preenchido | `GET /a-instituicao/sobre` | 200; título presente; texto presente; breadcrumb "A Instituição" presente. |
| P-2 | Slug inexistente | — | `GET /a-instituicao/nao-existe` | 404 (ou a view "conteúdo não encontrado", conforme a decisão de UX). |
| P-3 | Contador de cliques | conteúdo com `clicks = 0` | `GET /a-instituicao/sobre` | `clicks` passa a 1 no banco. |
| P-4 | Página de secretaria usa outra view | conteúdo com `category_contents_id = 7` | `GET /a-instituicao/secretarias/financeira` | Renderiza `portal.components.secretarias.show` (presença de `menus` / abas), **não** a view institucional. |
| P-5 | Página de órgão passa `sidenews` | conteúdo com `category_contents_id = 32` | `GET /a-instituicao/orgaos/orgao-x` | Renderiza a view institucional com o bloco de notícias do órgão. |
| P-6 | Galeria só aparece com `galery = 1` | 2 conteúdos: um com `galery = 1` + 2 arquivos, outro com `galery = 0` | GET nos dois | O primeiro exibe `id="lightgallery"`; o segundo não. |
| P-7 | Vídeo renderizado | conteúdo com `links.link` | `GET /a-instituicao/sobre` | `<iframe>` com o `src` de `embedVideo()` presente. |
| P-8 | Layout inexistente derruba com 404 | `configurations.layout = 'default'` | `GET /a-instituicao/sobre` | 404 com `view_inexistente` **e** `configurations.pagina_em_manutencao = 1` (comportamento atual; na v7 deve ser um erro de configuração mais explícito). |

## 5. Casos de teste — CMS

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C-1 | Listagem exige permissão | role sem `a-instituicao.index` | `GET /cms/conteudos/a-instituicao` | 302 (redireciona) e **não** 200. |
| C-2 | Listagem visível com permissão | role com `a-instituicao.index` | `GET /cms/conteudos/a-instituicao` | 200; título "Lista de conteudos da Instituição"; tabela `tabela_a_instituicao`. |
| C-3 | Botão "Cadastrar" só aparece com permissão | role com `.index` mas sem `.create` | `GET /cms/conteudos/a-instituicao` | 200 e **sem** o link `cadastrar/a-instituicao`. |
| C-4 | Grade JSON traz as permissões por linha | — | `GET /cms/datatable/a_instituicao` | JSON com `value, titulo, id, galeria, slug, edit, delete, active, inactivate`; `edit = false` para role sem `.edit`. |
| C-5 | Criar exige `title` | — | `POST /cms/conteudo` sem `title` | Redirect com erro em `title`: "Por favor, informe um título." |
| C-6 | Criar exige `text` | — | `POST /cms/conteudo` sem `text` | Erro em `text`: "Por favor, digite algum texto." |
| C-7 | Criar exige `category_contents_id` | — | `POST /cms/conteudo` sem `category_contents_id` | Erro em `category_contents_id`. |
| C-8 | Limite de 120 caracteres do título | — | `POST /cms/conteudo` com `title` de 121 caracteres | Erro em `title` (max). |
| C-9 | Slug gerado a partir do título | — | `POST /cms/conteudo` com `title = "Sobre a Prefeitura"` | `contents.slug = 'sobre-a-prefeitura'`. |
| C-10 | Colisão de slug | 1 conteúdo com slug `sobre` | `POST /cms/conteudo` com `title = "Sobre"` | Novo slug `sobre-<maxid+1>`; o conteúdo antigo **não** é alterado. |
| C-11 | Exclusão limpa `menu_items` | conteúdo com 2 `menu_items` vinculados | `DELETE /cms/conteudo/{id}` | Conteúdo removido (soft delete) e **0** `menu_items` apontando para ele. |
| C-12 | Exclusão em lote | 2 conteúdos marcados | `POST /cms/ajax/generic/content/deleteAll` | Ambos removidos. |
| C-13 | Módulo desligado oculta o menu | `modulos.status = false` para `a-instituicao` | `GET /cms/dashboard` (ou a página que renderiza o menu) | A entrada "A Instituição" **não** aparece no menu lateral. |
| C-14 | Slug inexistente redireciona | — | `GET /cms/conteudos/slug-inexistente` | Redirect para `dashboard.index` (`genericIndex` cai no `default`). |

## 6. Casos de teste — Unitários

| # | Caso | Assert |
|---|---|---|
| U-1 | `ContentRequest` escolhe o case pela **categoria**, não pela rota | Requisição com `category_contents_id` da categoria 6 aplica as regras de `a-instituicao` (título 120, `link` 200, `archive_highlight` mimes). |
| U-2 | `MenuItemPresenter::getLink()` para a categoria 6 | Retorna `url('/a-instituicao/' . $content->slug)`. |
| U-3 | `MenuItemPresenter::getLink()` para as categorias 7 e 32 | Retorna `url('a-instituicao/' . $category->slug . $content->slug)`. |
| U-4 | `getContentDataWith()` carrega as relações esperadas | `archiveHighlight`, `archiveGallery` e `categoryContents:id,name,slug` presentes. |
| U-5 | `updateClicks()` incrementa | `clicks` de 7 → 8. |
| U-6 | `getTemplateActived()` com layout sem `home/index` | Lança `NotFoundHttpException` e marca `pagina_em_manutencao`. |

## 7. Testes de regressão dos bugs (ligados a `08-GAPS-E-DIVIDAS.md`)

| # | Bug | Teste que deve falhar hoje e passar depois da correção |
|---|---|---|
| B-1 | Formulário exibe URL de A Cidade | Renderizar `prefecture.contents.a-instituicao.edit` e afirmar que a saída contém `/a-instituicao/` e **não** contém `/a-cidade`. |
| B-2 | Nome de rota duplicado | Afirmar que `Route::has('a-instituicao.index')` aponta para `/a-instituicao/{slug}` e que `route('a-instituicao.index', 'sobre')` retorna `.../a-instituicao/sobre` (hoje retorna `.../a-instituicao/secretarias/sobre`). |
| B-3 | Rota `a-instituicao-secretarias.index` inexistente | `GET /portal/secretarias/financeira` deve redirecionar (hoje estoura `RouteNotFoundException` → 500). |
| B-4 | Coluna `slug` ausente na view `a_instituicao` | A migration `2024_11_29_100640` deve ter rodado: `information_schema.columns` deve conter `slug` na view. |
| B-5 | `modulos.status` inconsistente | Afirmar que `modulos` com slug `a-instituicao` tem `status = true` (e que os demais módulos têm status coerente com a decisão do negócio). |

## 8. Como rodar

```bash
# a partir de v6/
cd v6

# suíte completa
./vendor/bin/phpunit

# apenas um arquivo
./vendor/bin/phpunit tests/Feature/Instituicao/InstituicaoPublicPageTest.php

# apenas um método
./vendor/bin/phpunit --filter testInstituicaoPageRenderiza

# suíte Unit
./vendor/bin/phpunit --testsuite=Unit
```

Variáveis de ambiente esperadas (`.env` de teste):

```
APP_ENV=testing
DB_CONNECTION=pgsql
DB_HOST=<host do postgres de teste>
DB_DATABASE=prefeituras_v5
DB_USERNAME=postgres
CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_DRIVER=sync
```

No ambiente de desenvolvimento, a base de teste pode ser um schema separado:

```bash
# exemplo ilustrativo
createdb prefeituras_test
php artisan migrate --env=testing
php artisan db:seed --class=InstituicaoTestSeeder --env=testing
```

## 9. Dados de teste do legado (para replicação)

Conteúdos existentes na base atual (consulta de leitura), úteis como massa de
dados para testes de regressão de layout:

| id | title | slug |
|---|---|---|
| 5 | Prefeito | prefeito |
| 6 | Vice-prefeito | vice-prefeito |
| 7 | Aspectos Gerais | aspectos-gerais |
| 8 | Localização | localizacao |
| 9 | Sobre | sobre |

## 10. Critérios de aceite da suíte do módulo

- [ ] Nenhum teste depende da **ordem** de execução (usar `DatabaseTransactions`).
- [ ] Nenhum teste depende do `Cache::rememberForever('modulos')` já aquecido por
      outro teste — limpar o cache no `setUp`.
- [ ] Nenhum teste escreve em arquivo fora de `storage/framework/testing`.
- [ ] Todo teste que exercise a grade do CMS declara explicitamente a quantidade
      de linhas esperadas (a view é pequena, mas o `row_number()` renumera a cada
      consulta).
- [ ] Os testes de rota duplicada (`B-2`) e de rota inexistente (`B-3`) ficam
      **marcados como falhos** até a correção — serves de documentação viva do bug.
