# Secretarias — Testes

> Estratégia para validar o módulo **Secretarias** na v6 (regressão) e na v7
> (reconstrução). O repositório v6 usa PHPUnit 6 (`v6/composer.json`), Laravel 5.5 e
> banco PostgreSQL de teste; `v6/phpunit.xml` define os suites `Feature` e `Unit`.

---

## 1. Estratégia

| Camada | Ferramenta | Cobertura do módulo |
|---|---|---|
| **Unit** | PHPUnit | Regras puras: normalização de slug, ordenação de departamentos, cálculo do limite de notícias |
| **Feature (HTTP)** | PHPUnit + `Illuminate\Foundation\Testing\TestCase` | Rotas do CMS, do portal e da API; validação dos Form Requests; permissões |
| **Feature (BD)** | `RefreshDatabase` | Integridade dos relacionamentos e dos uploads |
| **Regressão manual** | Checklist no fim do arquivo | Comportamento observável no navegador (CMS AdminLTE e portal) |

Convenções a seguir na v6:

- `tests/TestCase.php` e `tests/CreatesApplication.php` já existem.
- Usar `Sentinel` diretamente (`Sentinel::login()` / `Sentinel::logout()`) para
  simular login — o módulo não usa o guard `web` do Laravel.
- Configurar `CACHE_DRIVER=array` (já no `phpunit.xml`) para que
  `Cache::rememberForever('modulos')` do `ModulosServiceProvider` não persista
  entre testes.

## 2. Fixtures e factories necessárias

### 2.1 Categorias e conteúdo base

```php
// tests/Feature/Secretarias/SecretariaTestCase.php
namespace Tests\Feature\Secretarias;

use App\Prefecture\CategoryContent;
use App\Prefecture\Content;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class SecretariaTestCase extends BaseTestCase
{
    protected function createCategoriaSecretarias(): CategoryContent
    {
        return CategoryContent::create([
            'name' => 'Secretarias',
            'slug' => 'secretarias',
            'category_contents_id' => null,
            'status' => true,
        ]);
    }

    protected function createSecretaria(string $title = 'Secretaria da Saúde', string $slug = 'saude'): Content
    {
        $categoria = $this->createCategoriaSecretarias();

        return Content::create([
            'title' => $title,
            'slug' => $slug,
            'text' => '<p>Texto</p>',
            'category_contents_id' => $categoria->id,
            'status' => true,
            'clicks' => 0,
        ]);
    }
}
```

### 2.2 Módulo ligado (para os testes de menu do CMS e select de notícias)

```php
protected function enableModuloSecretarias(): void
{
    \App\Http\Model\Admin\Modulos::updateOrCreate(
        ['slug' => 'secretarias'],
        ['name' => 'Secretarias', 'status' => true]
    );

    // o ModulosServiceProvider usa Cache::rememberForever
    Cache::forget('modulos');
}
```

### 2.3 View SQL de teste

As views `secretarias`, `secretarias_categorias` e `secretarias_paginas` **não são
tabelas**: o `RefreshDatabase` recria apenas o schema das tabelas, e as views vêm das
migrations. Garanta que as migrations de view rodem no ambiente de teste; se o teste
usar `DatabaseMigrations` (e não `RefreshDatabase`), as views são criadas.

Para um teste **unitário** das queries da view, criar a view direto:

```php
DB::statement("
    CREATE OR REPLACE VIEW secretarias AS
    SELECT row_number() OVER (ORDER BY c.created_at ASC) AS value,
           c.title as titulo, c.id as id, c.galery AS galeria, c.slug as slug
    FROM contents c
    JOIN category_contents cc ON cc.id = c.category_contents_id
    WHERE c.deleted_at IS null and cc.slug = 'secretarias'
    ORDER by c.created_at desc;
");
```

## 3. Casos de teste — CMS (secretarias)

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | Grade de secretarias | 3 conteúdos na categoria 7 | `GET /cms/datatable/secretarias` | 200; `data[0].titulo`, `data[0].slug`, `data[0].galeria`; `data[0].edit`/`delete` presentes |
| C2 | Listagem de secretarias | idem | `GET /cms/conteudos/secretarias` | 200; view `prefecture.contents.secretarias.index`; tabela `tabela_secretarias` |
| C3 | Formulário de cadastro | — | `GET /cms/conteudos/cadastrar/secretarias` | 200; campo `acronym` presente; `types_sizes` disponível |
| C4 | Criar secretaria válida | `title`, `category_contents_id`, `text` | `POST /cms/conteudos` | 302; `contents.slug` = `Str::slug($title)`; `contents.acronym` gravado |
| C5 | Criar secretaria sem título | — | `POST /cms/conteudos` com `title` vazio | 302 de volta; erro "title is required"; nada gravado |
| C6 | Slug duplicado | já existe conteúdo com slug `saude` | `POST /cms/conteudos` com título "Saúde" | slug gerado `saude-<maxid+1>` |
| C7 | Título > 120 chars | — | `POST /cms/conteudos` | falha de validação (`max:120`) |
| C8 | Destaque sem tipo válido | `archive_highlight` = `.exe` | `POST /cms/conteudos` | falha `mimes:jpg,jpeg,png,gif,webp` |
| C9 | Tamanho personalizado | `type_size_id = 4` sem `width`/`height` | `POST /cms/conteudos` | falha: `width` e `height` obrigatórios |
| C10 | Tamanho personalizado válido | `type_size_id = 4`, `width=300`, `height=200` | `POST /cms/conteudos` | 302; `archives.width`/`height` gravados |
| C11 | Excluir secretaria remove `menu_items` | `menu_items.contents_id` = id | `DELETE /cms/conteudo/delete/{id}` | `menu_items` correspondente removido |
| C12 | Permissão negada | usuário sem `secretarias.index` | `GET /cms/conteudos/secretarias` | 302 para `login` ou para a rota anterior + flash "Você não tem autorização…" |

## 4. Casos de teste — CMS (departamentos)

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| D1 | Grade de departamentos | 2 categorias | `GET /cms/datatable/secretarias_categorias` | 200; `data[0].name`, `data[0].nome_secretaria` |
| D2 | Criar departamento | `name`, `secretarias_id` válido | `POST /cms/conteudo/secretarias/secretarias-categorias` | 302; linha em `categorias_secretarias` |
| D3 | Criar departamento sem nome | — | `POST` sem `name` | falha: "O nome do departamento é obrigatório." |
| D4 | `secretarias_id` = "0" | — | `POST` com `secretarias_id=0` | falha do `SelectRule` ("A secretaria é obrigatória.") |
| D5 | Editar departamento | id existente | `PUT /cms/conteudo/secretarias/secretarias-categorias/{id}` | 302; `name` atualizado |
| D6 | Excluir departamento | id existente | `DELETE .../{id}` | JSON `{"success": …}`; linha removida (soft delete) |
| D7 | Exclusão em lote | 2 ids | `POST /cms/secretarias/secretarias-categorias` com `toDelete[]` | JSON `{"success": …}`; ambas removidas |
| D8 | Nome duplicado permitido (comportamento v6) | já existe "Obras" na secretaria 1 | `POST` com o mesmo `name` e `secretarias_id` | **302 e linha criada** (a deduplicação da v5 foi removida) |

## 5. Casos de teste — CMS (páginas)

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Grade de páginas | 1 página com categoria | `GET /cms/datatable/secretarias_paginas` | 200; `data[0].title`, `data[0].categoria`, `data[0].nome_secretaria` |
| P2 | Criar página | `title`, `categoria_id`, `text` | `POST /cms/conteudo/secretarias/secretarias-paginas` | 302; linha em `paginas_secretarias` |
| P3 | Criar página sem título | — | `POST` sem `title` | falha **sem** a mensagem "O nome do departamento…" (bug da chave `name.required`); a validação continua barrando |
| P4 | Criar página com destaque | `archive_highlight` = PNG | `POST` | `archives` com `highlight = true`; arquivo em `storage/app/public/content/secretarias_paginas/{id}/destaque/` |
| P5 | Criar página com anexos | `archive[0][0]` + `archive[0][description]` | `POST` | `archives` com `highlight = false` e `description` gravada; arquivo em `.../arquivos/` |
| P6 | Trocar a imagem de destaque | página já com destaque | `PUT` com novo `archive_highlight` | destaque antigo removido (`archives` com `highlight=true` = 1) |
| P7 | Reenviar anexos | página com 1 anexo | `PUT` com 1 anexo novo | ⚠ **2 arquivos** (comportamento atual — o legado não limpa os anteriores) |
| P8 | Excluir anexo | id de `archives` | `DELETE /cms/secretarias_paginas/arquivo/{id}` | JSON `{"success": …}`; arquivo removido do disco e da linha |
| P9 | Página sem `categoria_id` | — | `POST` | falha do `SelectRule` |

## 6. Casos de teste — CMS (menu das secretarias)

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| M1 | Abrir menu | secretaria id 1 | `GET /cms/conteudo/secretarias/menus/1` | 200; view `prefecture.contents.secretarias.menu.index`; `$secretaria->titulo` na caixa |
| M2 | JSON de categorias | 2 categorias com `order` 2 e 1 | `GET /cms/get_categorias/secretarias/1` | JSON com 2 itens **ordenados por `order` asc**; `contents_category` aninhado |
| M3 | Reordenar | categorias com `order` 1 e 2 | `POST /cms/order_categorias/secretarias` com `ids[] = [2,1]` | resposta = string `"true"`; `order` reescrito para 1 e 2 respectivamente |
| M4 | Reordenar com id inválido | — | `POST` com `ids[] = [999]` | resposta = string `"false"` (o `catch (\Throwable)` engole o erro) |
| M5 | Criar departamento pela tela do menu | — | `POST /cms/conteudo/secretarias/secretarias-categorias` com `secretarias_id` | 302/200; categoria criada e listada no JSON |

## 7. Casos de teste — Portal

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| T1 | Página da secretaria | conteúdo categoria 7, slug `saude` | `GET /a-instituicao/secretarias/saude` | 200; título; destaque; texto |
| T2 | Slug inexistente | — | `GET /a-instituicao/secretarias/nao-existe` | 404 / view de conteúdo não encontrado (sem 500) |
| T3 | Menu de departamentos | 1 departamento + 2 páginas | `GET /a-instituicao/secretarias/saude` | HTML contém `route('secretarias-pages.index')` e os títulos das páginas |
| T4 | Sem departamentos | nenhuma categoria | `GET /a-instituicao/secretarias/saude` | HTML **não** contém o menu; coluna de conteúdo com `col-12` |
| T5 | Notícias da secretaria | 15 notícias com `secretaria = 'saude'` | `GET /a-instituicao/secretarias/saude` | **12** itens no HTML (corte por `splice(0, 12)`) |
| T6 | Notícias futuras | 3 notícias com `publication_date` no futuro | `GET /a-instituicao/secretarias/saude` | 0 notícias no HTML |
| T7 | Clique contabilizado | `clicks = 5` | `GET /a-instituicao/secretarias/saude` | `clicks` = 6 |
| T8 | Eventos | 2 eventos futuros da secretaria | `GET /a-instituicao/secretarias/saude` | 2 eventos; `eventsList` limitado a 5 |
| T9 | Página institucional | 1 página com texto e 1 anexo | `GET /a-instituicao/secretarias/paginas/saude/1` | 200; título; texto; link "Arquivos para Download" |
| T10 | Página inexistente | — | `GET /a-instituicao/secretarias/paginas/saude/999` | ⚠ hoje: **erro 500** (o `find($id)` retorna null e a view acessa `$content->title`) — registrar como **bug conhecido** |
| T11 | Slug de página de outra secretaria | página da secretaria 2, slug da secretaria 1 | `GET /a-instituicao/secretarias/paginas/saude/1` | ⚠ hoje: exibe a página de outra secretaria (sem validação) |
| T12 | Rota `portal/secretarias/...` | — | `GET /portal/secretarias/saude` | ⚠ hoje: **erro 500** (`Route [a-instituicao-secretarias.index] not defined`) — bug conhecido |
| T13 | Breadcrumb | — | `GET /a-instituicao/secretarias/saude` | breadcrumb "A Instituição › Secretarias › {título}" |
| T14 | Link do menu do portal | `menu_items` com `category_contents_id = 7` | render da home | link = `/a-instituicao/secretarias/{slug}` (via `MenuItemPresenter`) |

## 8. Casos de teste — API

| # | Cenário | Act | Assert |
|---|---|---|---|
| A1 | Listagem | `GET /api/v1/secretarias` | 200; `data` com 13 itens; cada item com `titulo`, `slug`, `interacoes` |
| A2 | Limite | `GET /api/v1/secretarias?limite=2` | `data` com 2 itens |
| A3 | Filtro por título | `GET /api/v1/secretarias?titulo=saude` | ⚠ **hoje retorna todos** (o `whereRaw` é descartado) — bug a ser coberto por teste de regressão na v7 |
| A4 | Filtro por descrição | `GET /api/v1/secretarias?descricao=saúde` | ⚠ idem A3 |
| A5 | Descrição sem HTML | conteúdo com `<p>texto</p>` | `data[0].descricao` = `texto` (sem tags) |
| A6 | Arquivos | secretaria com `archives` | `data[0].arquivos[0].url` presente |

## 9. Testes de performance (regressão)

Objetivo: garantir que a v7 não reproduza os problemas medidos no legado.

| # | Métrica | Legado (v6) | Meta (v7) | Como medir |
|---|---|---|---|---|
| PF1 | Queries na grade de secretarias | 1 (view inteira) + 4×N `has_access()` | ≤ 3 (count + page + eager loads) | `DB::listen()` / `assertDatabaseQueryCount()` |
| PF2 | Linhas materializadas na grade | 13 secretarias (100% da view) | 25 (tamanho da página) | `Log::listen` ou `EXPLAIN ANALYZE` |
| PF3 | Queries na página da secretaria | 1 (secretarias) + 1 (notícias paginado) + 1 (hasMany sem limite) + N×2 (lazy loading das notícias) | ≤ 6, com `limit(12)` no SQL | contagem de queries |
| PF4 | Tempo da página da secretaria | — | < 200 ms com 1000 notícias | `php artisan benchmark` ou `k6`/`ab` |
| PF5 | Payload do endpoint da API | 13 secretarias completas | paginado | `assertJsonCount` |

Comando de referência (Laravel 11/12):

```bash
php artisan test --filter=Secretaria
php artisan test --filter=SecretariaPerformance
```

## 10. Testes manuais (checklist de regressão no CMS)

1. Abrir `/cms/conteudos/secretarias` → listar as 13 secretarias; exportar Excel e PDF.
2. Clicar em "Acessar secretaria" → abre `/a-instituicao/secretarias/{slug}` em nova aba.
3. Clicar em "Menus das secretarias" → abre a tela de menu da secretaria.
4. Criar um departamento → verificar na grade de departamentos.
5. Arrastar o departamento na caixa "Menu de {secretaria}" → a ordem persiste após F5.
6. Criar uma página com imagem e 1 anexo → verificar os arquivos no storage.
7. Editar a página e trocar a imagem → a imagem antiga é substituída.
8. Verificar se o módulo "Secretarias" aparece no menu lateral do CMS
   (depende de `modulos.status = true` + `Cache::forget('modulos')`).
9. No formulário de **Notícias**, confirmar que o select "Secretaria" aparece
   (mesma dependência de `modulos.status`).
10. Publicar uma notícia com a secretaria selecionada → conferir em
    `/a-instituicao/secretarias/{slug}` se ela aparece em "Notícias de {título}".

## 11. Como rodar

### v6 (legado)

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

# Requer PostgreSQL de teste configurado em .env.testing
php vendor/bin/phpunit --testsuite Feature
php vendor/bin/phpunit --filter SecretariaTest
php vendor/bin/phpunit --filter SecretariaApiTest

# Só um arquivo
php vendor/bin/phpunit tests/Feature/Secretarias/SecretariaCmsTest.php
```

### v7 (reconstrução)

```bash
cd /home/one/projects/maxima/v7/v7   # [NÃO VERIFICADO] — caminho da v7 a definir

php artisan test --filter=Secretaria
php artisan test --coverage --min=80
```

### Regressão manual do portal

```bash
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/a-instituicao/secretarias/saude
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/api/v1/secretarias
```

## 12. Bugs que os testes devem **documentar** (não apenas falhar)

| Bug | Teste que o evidencia |
|---|---|
| `a-instituicao-secretarias.index` inexistente | T12 |
| Nome de rota `a-instituicao.index` duplicado | Teste de regressão do breadcrumb (T13) e do menu (T14) |
| `whereRaw` descartado na API | A3, A4 |
| `categoria()` com `'teste'` concatenado | Teste unitário do model (o método retorna string, não relação) |
| Mensagem trocada no `SecretariasPaginasRequest` | P3 |
| Módulo desabilitado impedindo o vínculo notícia↔secretaria | Teste de integração do `ModulosServiceProvider` |
| Tabelas de submenus vazias | Teste de sanidade do dataset (`assertDatabaseCount('categorias_secretarias', 0)`) — vira fixture na v7 |
| Duplicação de anexos ao editar página | P7 |
| `inconsistência` da view (`row_number` ASC × `ORDER BY` DESC) | Teste que verifica a correlação entre `value` e a ordem exibida |
