# Comitê de Investimentos — Estratégia de Testes

## 1. Situação atual

O módulo **não tem nenhum teste** na v6 nem na v5.

| Verificação | Resultado |
|---|---|
| `v6/tests/Feature` | Apenas testes do Dashboard (`Dashboard*Test.php`) |
| `v6/tests/Unit` | Sem testes do comitê |
| `v5/tests` | Apenas `ExampleTest.php` e `CnpjAlphanumericValidationTest.php` |

Infraestrutura disponível:

- `v6/phpunit.xml` — suítes `Feature` e `Unit`, `bootstrap="vendor/autoload.php"`,
  `APP_ENV=testing`, `CACHE_DRIVER=array`, `SESSION_DRIVER=array`,
  `QUEUE_DRIVER=sync`. **Não há `<env name="DB_CONNECTION">`**, ou seja, os
  testes rodam contra a conexão configurada em `v6/config/database.php`.
- `v6/tests/TestCase.php` — classe base mínima.
- `v6/tests/CreatesApplication.php` — bootstrap da aplicação.
- `v6/tests/DashboardTestCase.php` — base com helpers de módulos, papéis e
  usuários (`setModules`, `createRole`, `createUser`) que pode servir de modelo
  para os helpers do comitê.

## 2. Estratégia proposta para a v7

| Tipo | Escopo | Ferramenta |
|---|---|---|
| **Feature (portal)** | Rotas públicas, filtros, paginação, redirect, incremento de cliques | `tests/Feature/Portal/ComiteDeInvestimentosTest.php` |
| **Feature (CMS)** | Permissões, CRUD, validação, exclusão em lote, DataTable | `tests/Feature/Cms/ComiteDeInvestimentosAdminTest.php` |
| **Feature (view SQL)** | Colunas da view `comite_de_investimentos` | `tests/Feature/Cms/ComiteDeInvestimentosViewTest.php` |
| **Unit** | Normalização de texto, filtro de ano/mês | `tests/Unit/ComiteFiltroTest.php` |
| **Segurança** | Tentativa de SQL injection via `ano` e `texto` | `tests/Feature/Security/ComiteSqlInjectionTest.php` |

## 3. Casos de teste concretos

### 3.1 Portal — listagem e filtros

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | Categoria-pai `comite-de-investimentos` + 2 subcategorias (orders 1 e 2) | `GET /comite-de-investimentos` | Redirect para `portal.comite-de-investimentos.show` com o slug da subcategoria de `order = 1` |
| P2 | Subcategoria `atas-2024` sem atas | `GET /comite-de-investimentos/atas-2024` | Status 200; view `portal.layouts.{layout}.comite-de-investimentos.show`; texto "Nenhum resultado." |
| P3 | 15 atas publicadas na subcategoria | `GET /comite-de-investimentos/atas-2024` | 12 itens na 1ª página; paginação com 2 páginas |
| P4 | 3 atas, uma com `publication_date` futura | `GET /comite-de-investimentos/atas-2024` | Apenas 2 atas listadas; a futura não aparece |
| P5 | Atas em 2023 e 2024 | `GET /comite-de-investimentos/atas-2024?ano=2024` | Somente atas de 2024; seletor de mês preenchido |
| P6 | Atas em janeiro e março de 2024 | `GET /comite-de-investimentos/atas-2024?ano=2024&mes=3` | Apenas as de março |
| P7 | Ata com título "Comitê de Investimentos 2024" | `GET /comite-de-investimentos/atas-2024?texto=comite` | A ata aparece (busca sem acento) |
| P8 | Ata com corpo contendo "deliberação" | `GET /comite-de-investimentos/atas-2024?texto=deliberacao` | A ata aparece (busca no corpo, sem acento) |
| P9 | Slug inexistente | `GET /comite-de-investimentos/inexistente` | Redirect para `/publicacoes` |
| P10 | Requisição AJAX `X-Requested-With: XMLHttpRequest` | `GET /comite-de-investimentos/atas-2024?ano=2024` | Resposta JSON (`application/json`) contendo HTML renderizado |
| P11 | — | `GET /comite-de-investimentos?page=2` | A página 2 traz os itens 13 e 14 (garante que o `paginate(12)` é reaplicado no bloco de filtros) |

### 3.2 Portal — leitura

| # | Arrange | Act | Assert |
|---|---|---|---|
| L1 | Ata publicada com `clicks = 5` | `GET /comite-de-investimentos/atas-2024/ata-001` | Status 200; `contents.clicks` passa a 6 |
| L2 | Ata com `publication_date` futura | `GET /comite-de-investimentos/atas-2024/ata-futura` | Redirect para `portal.comite-de-investimentos.index`; `clicks` **não** incrementa |
| L3 | Ata com 2 anexos (`highlight = null`) e 1 PDF (`view_pdf = true`) | `GET /comite-de-investimentos/atas-2024/ata-001` | HTML contém o partial de arquivos e o título do PDF |
| L4 | Slug inexistente | `GET /comite-de-investimentos/atas-2024/nao-existe` | Resposta 500 (comportamento legado) **ou** 404 na v7 — registrar a mudança de contrato |

### 3.3 CMS — permissões

| # | Arrange | Act | Assert |
|---|---|---|---|
| A1 | Papel sem permissões, logado | `GET /cms/conteudos/comite-de-investimentos` | 403 (via `check.route.permission`) |
| A2 | Papel com `comite-de-investimentos.index` | `GET /cms/conteudos/comite-de-investimentos` | 200 |
| A3 | Papel com `comite-de-investimentos.create` | `GET /cms/conteudos/comite-de-investimentos` | O botão "Cadastrar Comitês de Investimentos" aparece no HTML |
| A4 | Papel sem `comite-de-investimentos.delete` | `GET /cms/conteudos/comite-de-investimentos` | O botão de exclusão em lote **não** aparece |
| A5 | `super-admin` | `GET /cms/conteudos/comite-de-investimentos` | Todos os botões aparecem |
| A6 | Papel sem permissão + `modulos.status = false` | `GET /cms` | O item "Comitê de Investimentos" **não** aparece no menu |

### 3.4 CMS — CRUD

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | — | `POST /cms/conteudo` com `title`, `category_contents_id`, `publication_date`, `text` | 302 para `listar-conteudo`; linha em `contents` com `slug` gerado |
| C2 | Payload sem `category_contents_id` | mesmo POST | Erro de validação; `contents` sem nova linha |
| C3 | Payload sem `publication_date` | mesmo POST | **Comportamento legado:** a ata é criada com `publication_date = null` e nunca aparece no portal. Na v7: erro de validação |
| C4 | Payload com `title` de 130 caracteres | mesmo POST | Erro de validação (regra `max:120`, apesar do formulário dizer 150) |
| C5 | Ata existente | `PUT /cms/conteudo/{id}` | Campos atualizados; `updated_at` alterado |
| C6 | Ata existente | `DELETE /cms/conteudo/delete/{id}` | `deleted_at` preenchido (soft delete) |
| C7 | 3 atas marcadas | `POST /cms/ajax/conteudo/deleteAll` com `toDelete[]` | As 3 têm `deleted_at` preenchido |
| C8 | — | `GET /cms/datatable/comite_de_investimentos` | JSON com `data[]`, `recordsTotal`, flags `edit`/`delete` por linha |

### 3.5 CMS — categorias

| # | Arrange | Act | Assert |
|---|---|---|---|
| K1 | — | `POST /cms/categoria-conteudo` com `name` e `category_contents_id` da categoria-pai | Categoria criada com `order` nulo |
| K2 | 3 subcategorias | `POST /cms/ajax/ajust-categorias` com `sortable_data[]` | `order` atualizado conforme a nova ordem |
| K3 | Subcategoria com atas | `DELETE /cms/categoria-conteudo/{id}` | Soft delete; as atas continuam apontando para a categoria (FK não é removida) |

### 3.6 View SQL

| # | Arrange | Act | Assert |
|---|---|---|---|
| V1 | 2 atas na subcategoria, 1 na categoria-pai, 1 com `deleted_at` | `SELECT * FROM comite_de_investimentos` | 2 linhas; a da categoria-pai e a excluída **não** aparecem |
| V2 | 1 ata | `SELECT categoria_slug, slug FROM comite_de_investimentos` | Retorna o slug da subcategoria e o slug da ata (colunas de nov/2024) |
| V3 | 1 ata com `publication_date` futura | `SELECT * FROM comite_de_investimentos` | A ata **aparece** na view (a view não filtra por data) — comportamento legado |
| V4 | 1 ata com `publication_date` nula | idem | A ata aparece com `data_publicacao` nula |

### 3.7 Segurança (prioridade máxima)

| # | Arrange | Act | Assert |
|---|---|---|---|
| S1 | — | `GET /comite-de-investimentos/atas-2024?ano=1%20OR%201=1` | Sem erro SQL; resposta 200; a lista não é expurgada |
| S2 | — | `GET /comite-de-investimentos/atas-2024?ano=1)%20UNION%20SELECT%20null,null,null,null--` | Sem erro SQL; nenhuma linha extraída |
| S3 | — | `GET /comite-de-investimentos/atas-2024?texto='%20OR%20'1'='1` | Sem erro SQL; busca tratada como literal |
| S4 | — | `GET /comite-de-investimentos/atas-2024?texto=%25` | O `%` é tratado como caractere literal (na v7) |
| S5 | Usuário anônimo | `POST /cms/conteudo` com os dados de uma ata | 401/403 — o cadastro exige login no CMS |

## 4. Fixtures e factories necessárias

```php
// (ilustrativo) database/factories/ComiteCategoryFactory.php
class ComiteCategoryFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->unique()->words(2, true),
            'slug' => fake()->unique()->slug(),
            'order' => 1,
            'category_contents_id' => null, // categoria-pai
        ];
    }
}
```

```php
// (ilustrativo) database/factories/ComiteMinuteFactory.php
class ComiteMinuteFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'text' => '<p>'.fake()->paragraph().'</p>',
            'slug' => fake()->unique()->slug(),
            'publication_date' => now()->subDay(),
            'clicks' => 0,
            'category_contents_id' => null, // preencher com a subcategoria
        ];
    }
}
```

Factories auxiliares: `CategoryContentFactory` (categoria-pai e subcategorias) e
`ArchiveFactory` (anexos com `highlight` e `view_pdf`).

## 5. Fixtures de view e seed para ambiente de teste

Criar uma migration/seeder de ambiente de teste que:

1. Insira a categoria-pai `category_contents` com `slug = 'comite-de-investimentos'`.
2. Insira 3 subcategorias (`Ata Ordinária`, `Ata Extraordinária`, `Relatório`).
3. Insira ~30 `contents` distribuídos em 2 anos, com mix de `publication_date`
   passadas, futuras e nulas, e com `clicks` variados.
4. Insira anexos: 2 por ata, sendo um deles `view_pdf = true`.
5. Garanta `modulos` id 48 com `status = true` e `menu_orders` com
   `slug = 'comite-de-investimentos'`, `modulo_id = 48`.

**Estado atual no banco de desenvolvimento:** categoria-pai id 39 existe; **0
subcategorias** e **0 contents** do comitê. O módulo está
`modulos.status = false`.

## 6. Como rodar

```bash
# Na raiz do projeto v6
cd v6

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

# Apenas o módulo
./vendor/bin/phpunit tests/Feature/Portal/ComiteDeInvestimentosTest.php
./vendor/bin/phpunit tests/Feature/Cms/ComiteDeInvestimentosAdminTest.php
./vendor/bin/phpunit tests/Feature/Security/ComiteSqlInjectionTest.php

# Por filtro
./vendor/bin/phpunit --filter ComiteDeInvestimentos
```

Regras do `phpunit.xml` a observar na v7:

- `DB_CONNECTION` precisa ser explicitamente definido (hoje não é) — usar
  `pgsql` apontando para um banco de teste descartável, ou `sqlite` em memória
  quando o schema for compatível.
- `CACHE_DRIVER=array` e `SESSION_DRIVER=array` evitam dependência de Redis.
- `QUEUE_DRIVER=sync` permite testar jobs de anexo sem worker.
