# Atas e Pontos de Frequência — Estratégia de Testes

> Versão analisada: `v6` (PHPUnit presente em `v6/vendor/bin/phpunit`,
> `v6/phpunit.xml` com suites `Feature` e `Unit`).
> Módulo **novo na v6** — hoje **não existe nenhum teste** cobrindo atas ou
> pontos de frequência.

---

## 1. Estratégia

| Tipo | Alvo | Ferramenta |
|---|---|---|
| Feature (HTTP) | Rotas do portal e do CMS, filtros, regras de visibilidade, increment de `clicks`, autorização | PHPUnit + `Illuminate\Foundation\Testing\TestCase` |
| Feature (segurança) | Injeção SQL via `ano` e `texto`, acesso a documento de outro módulo, acesso a documento removido | PHPUnit (payloads maliciosos) |
| Unit | Regras de filtro e normalização de texto (extração para service), mapeamento de `document_type` | PHPUnit puro |
| Integração (dados) | View SQL `atas_e_pontos`: paridade com `contents`, comportamento com soft delete | Teste de banco (PostgreSQL) |
| Manual | Download de anexo, exibição em iframe, comportamento mobile, permissões por papel | Checklist manual |

Estrutura de arquivos proposta (v7):

```
tests/Feature/Portal/AtasEPontosTest.php
tests/Feature/Portal/AtasEPontosSecurityTest.php
tests/Feature/Cms/AtasEPontosAdminTest.php
tests/Unit/Portal/AtasEPontosFilterTest.php
tests/Feature/Database/AtasEPontosViewTest.php
```

(os caminhos acima são **propostos**; ainda não existem no repositório)

Execução dentro do container da aplicação (o PHP não está no host):

```bash
cd /home/one/projects/maxima/v7/v6
docker compose exec app ./vendor/bin/phpunit --filter AtasEPontosTest
docker compose exec app ./vendor/bin/phpunit --testsuite Feature
docker compose exec app ./vendor/bin/phpunit --testsuite Unit
```

Verificação pontual no banco (somente `SELECT`):

```bash
docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 \
  -c "select count(*) from contents c join category_contents cc on cc.id = c.category_contents_id where cc.slug in ('atas','pontos-de-frequencia') and c.deleted_at is null;"
```

## 2. Fixtures necessárias

| Fixture | Campos mínimos | Observação |
|---|---|---|
| Categoria raiz | `name='Atas e Pontos de Frequência'`, `slug='atas-e-pontos'`, `category_contents_id=null` | id 41 em produção. |
| Categoria filha "Atas" | `slug='atas'`, `category_contents_id=<raiz>` | id 42. |
| Categoria filha "Pontos de Frequência" | `slug='pontos-de-frequencia'`, `category_contents_id=<raiz>` | id 43. |
| Conteúdo (ata) | `title`, `text`, `slug`, `publication_date`, `category_contents_id=<filha atas>`, `clicks=0` | `deleted_at=null`. |
| Conteúdo (ponto) | idem, na filha `pontos-de-frequencia` | — |
| Conteúdo futuro | `publication_date` = amanhã | Deve ser invisível. |
| Conteúdo removido | `deleted_at` preenchido | Comportamento da view SQL. |
| Arquivo | `archives` com `contents_id`, `path`, `extension='pdf'`, `type='application/pdf'`, `view_pdf=true` | `highlight=null`. |
| Usuário CMS | Sentinel, papel com `atas-e-pontos.create` e `atas-e-pontos.edit` | Usar `super-admin` para os testes de atalho. |

## 3. Casos de teste — Portal (feature)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | Raiz e ao menos uma categoria filha existem | `GET /atas-e-pontos` | Redireciona 302 para `/atas-e-pontos/atas` (primeira filha por `order`). |
| P2 | Nenhuma categoria filha (raiz removida das filhas) | `GET /atas-e-pontos` | Hoje: **erro 500**. Comportamento desejado: 404 ou redirecionamento institucional. Teste de regressão do bug 3.4. |
| P3 | Categoria filha "Atas" com 3 conteúdos | `GET /atas-e-pontos/atas` | 200; título da categoria exibido; 3 itens na lista. |
| P4 | Slug de categoria inexistente | `GET /atas-e-pontos/inexistente` | 302 para `/atas-e-pontos`. |
| P5 | 30 conteúdos na mesma categoria | `GET /atas-e-pontos/atas` | 12 itens por página e presença de links de paginação. |
| P6 | Conteúdos em 3 anos distintos | `GET /atas-e-pontos/atas?ano=2024` | Retorna apenas os de 2024; o seletor de ano traz todos os anos. |
| P7 | Conteúdos em vários meses de 2024 | `GET /atas-e-pontos/atas?ano=2024&mes=3` | Retorna apenas os de março/2024. |
| P8 | Conteúdo com título "Ata da Sessão Extraordinária" | `GET /atas-e-pontos/atas?ano=2024&texto=extraordinaria` | Retorna o item; a busca ignora acento (case-insensitive). |
| P9 | Conteúdo com `publication_date` no futuro | `GET /atas-e-pontos/atas` | O conteúdo futuro **não** aparece na lista. |
| P10 | Conteúdo futuro | `GET /atas-e-pontos/atas/slug-do-futuro` | 302 para `/atas-e-pontos`. |
| P11 | Conteúdo publicado com `clicks = 4` | `GET /atas-e-pontos/atas/slug` | 200 e `contents.clicks == 5`. |
| P12 | Conteúdo com anexo `view_pdf = true` | `GET /atas-e-pontos/atas/slug` | HTML contém `<iframe` com o `path` do arquivo. |
| P13 | Conteúdo com anexo `highlight = true` | `GET /atas-e-pontos/atas/slug` | O anexo de destaque **não** aparece em "Arquivos para Download". |
| P14 | Conteúdo com anexo PDF comum | `GET /atas-e-pontos/atas/slug` | Lista de downloads contém `url($archive->path)`; o `view_pdf` não aparece na lista (desktop). |
| P15 | Qualquer conteúdo | `GET /atas-e-pontos/atas` com header `X-Requested-With: XMLHttpRequest` | Resposta 200 com `Content-Type` JSON e corpo contendo o HTML da listagem. |
| P16 | Conteúdo de **outro** módulo com slug "noticia-x" | `GET /atas-e-pontos/atas/noticia-x` | Hoje: **200** (vazamento). Comportamento desejado: 404. Teste de regressão do bug 3.5. |
| P17 | Slug inexistente | `GET /atas-e-pontos/atas/nao-existe` | Hoje: **erro 500**. Comportamento desejado: 404. Teste de regressão do bug 3.5. |

## 4. Casos de teste — Segurança

| # | Arrange | Act | Assert |
|---|---|---|---|
| S1 | Nenhum | `GET /atas-e-pontos/atas?ano=2024)%20OR%201=1--` | **200**, sem erro de SQL; a resposta não traz documentos de outros anos. (Hoje: payload é concatenado no SQL cru.) |
| S2 | Nenhum | `GET /atas-e-pontos/atas?ano=2024)%20UNION%20SELECT%20null,null,null,null,null,null--` | Sem vazamento de colunas de outras tabelas; resposta 200 e lista vazia/filtrada. |
| S3 | Nenhum | `GET /atas-e-pontos/atas?ano=abc` | 200 (ou 422 na v7 com validação) e **sem** exceção de banco. |
| S4 | Nenhum | `GET /atas-e-pontos/atas?texto='` | **200** sem erro 500 (hoje quebra a query por falta de escape). |
| S5 | Nenhum | `GET /atas-e-pontos/atas?texto=%27%3B%20DROP%20TABLE%20contents%3B--` | Tabela `contents` intacta; resposta 200. |
| S6 | Conteúdo com `deleted_at` preenchido e categoria **raiz** | `SELECT * FROM atas_e_pontos` | O conteúdo removido **não** deve aparecer (hoje aparece por causa do `or` na view SQL). Teste de regressão do bug 3.3. |
| S7 | Anexo de conteúdo | `GET /storage/.../arquivo.pdf` direto | Seria 200 hoje (sem rota de download). Comportamento desejado na v7: servir via rota autorizada. |
| S8 | Usuário sem a permissão `atas-e-pontos.create` | `GET /cms/conteudos/cadastrar/atas-e-pontos` | Redireciona com aviso de autorização (middleware `check.route.permission`). |

## 5. Casos de teste — CMS

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | Operador com permissão | `GET /cms/conteudos/atas-e-pontos` | 200; título exibido é **"Atas e Pontos de Frequência"** (hoje sai "Unidades de Atendimento" — regressão do bug 3.6). |
| C2 | Idem | `GET /cms/datatable/atas_e_pontos` | JSON Yajra com `data[]` contendo `titulo`, `categoria`, `data_publicacao`, `categoria_slug`, `slug`, `id` e as flags `edit`/`delete`. |
| C3 | Conteúdo na categoria filha | `GET /cms/datatable/atas_e_pontos` | `categoria` = "Atas" e `categoria_slug` = "atas". |
| C4 | Operador sem `atas-e-pontos.create` | `GET /cms/conteudos/atas-e-pontos` | Botão "Cadastrar Conteúdo" ausente do HTML. |
| C5 | Operador sem `atas-e-pontos.delete` | idem | Checkbox mestre e botão de exclusão em lote ausentes. |
| C6 | Formulário submetido sem `title` | `POST /cms/conteudo` | 422/redirect com erro em `title` (regra `default` do `ContentRequest`). |
| C7 | Formulário com arquivo `.exe` | `POST /cms/conteudo` | Rejeitado pela regra `archive.*.0` (mimes). |
| C8 | Categoria filha criada | `GET /cms/categorias/atas-e-pontos` | A nova categoria aparece; `category_contents_id` aponta para a raiz. |
| C9 | Operador abre a listagem | `GET /cms/conteudos/atas-e-pontos` | `menu_orders.clicks` para `slug='atas-e-pontos'` é incrementado. |

## 6. Casos de teste — Integração com a view SQL

| # | Arrange | Act | Assert |
|---|---|---|---|
| V1 | Conteúdos publicados e removidos em raiz e filhas | `SELECT count(*) FROM atas_e_pontos` | Apenas conteúdos **não removidos**; o total confere com `contents` filtrado pelas categorias do módulo. |
| V2 | Conteúdos em 2 categorias | `SELECT categoria, count(*) FROM atas_e_pontos GROUP BY categoria` | Dois grupos ("Atas", "Pontos de Frequência"). |
| V3 | Conteúdos com a mesma data | `SELECT value FROM atas_e_pontos` | `value` é sequencial (1..N) e ordenado por `publication_date` desc. |
| V4 | Conteúdo | `SELECT data_publicacao FROM atas_e_pontos` | Texto no formato `DD/MM/YYYY às HH24:MI`. |

## 7. Casos de teste — Unidade

| # | Alvo | Arrange | Act | Assert |
|---|---|---|---|---|
| U1 | Normalização do termo de busca | Termo "Sessão Extraordinária" | `clean_slug($termo)` | Termo sem acentos e sem espaços duplos, preservando a semântica de busca. |
| U2 | Extração de ano/mês | Data `2024-03-15 10:00` | Helper de extração | Ano 2024, mês 3. |
| U3 | Mapeamento de tipo | Categoria filha `pontos-de-frequencia` | Mapeamento para `document_type` | `ponto_frequencia`; `atas` → `ata`. |
| U4 | Regra de visibilidade | `published_at` futuro | `scopePublished` | Documento excluído do conjunto. |

## 8. Checklist manual (portal)

1. `/atas-e-pontos` → abre a primeira categoria filha.
2. Selecionar um ano → a lista recarrega sem recarregar a página (Ajax).
3. Selecionar um mês → idem; o mês só aparece após escolher o ano.
4. Digitar um termo com acento → o resultado aparece (busca sem acento).
5. Paginar → o número vai para o `hash` da URL e a lista recarrega.
6. Abrir um documento → o contador de cliques aumenta em 1.
7. Verificar o PDF embutido no desktop e o link de download no celular.
8. Tentar abrir um documento futuro pela URL → redireciona.

## 9. Checklist manual (CMS)

1. Habilitar o módulo em `/cms/modulos` (id 46, `status = 0` por padrão).
2. Criar a categoria filha "Pontos de Frequência".
3. Publicar um documento com anexo PDF e marcar o arquivo para exibição
   embutida.
4. Conferir a linha no DataTable e o botão "Acessar conteúdo".
5. Excluir em lote e conferir o soft delete.
6. Conferir no log de auditoria o título gravado (hoje errado).

## 10. Cobertura esperada após a correção

| Item | Teste |
|---|---|
| P2, P16, P17 | Eliminam os erros 500 do portal. |
| S1..S5 | Eliminam a injeção SQL. |
| S6 | Elimina a exposição de conteúdo removido. |
| C1 | Elimina o título errado. |
| V1..V4 | Garantem a paridade da listagem do CMS. |
