# Solicitações LAI (e-SIC) — Estratégia de Testes

> A v6 **não possui** nenhum teste específico de LAI: a suíte existente em
> `v6/tests/` cobre apenas Dashboard e configuração do CMS
> (`v6/tests/Feature/Dashboard*.php`, `v6/tests/Unit/ExampleTest.php`). O que
> segue é o plano de testes para a v7, derivado do comportamento observado.

---

## 1. Estratégia

| Camada | Ferramenta | Escopo |
|---|---|---|
| **Feature (HTTP)** | PHPUnit + `RefreshDatabase` | Rotas públicas e do CMS: código de resposta, payload, redirecionamentos, e-mails disparados. |
| **Feature (UI/CMS)** | Laravel Dusk / Playwright (o repositório já tem Playwright em `v6/tests-e2e/`) | Fluxo do operador: listar por status, abrir detalhe, responder, indeferir, prorrogar. |
| **Unit** | PHPUnit | `DataMaskTrait` (regex), cálculo de prazo, gerador de protocolo, `enum StatusLai`. |
| **Integração (banco)** | PHPUnit + PostgreSQL de teste | Views SQL e scopes de prazo (o `groupBy` inválido só falha no PostgreSQL real). |
| **Contrato (API)** | PHPUnit com asserts de JSON Schema | Formato dos recursos de API, especialmente o que **não** pode ser exposto. |

Comandos (a definir no projeto v7):

```bash
php artisan test --filter=InformationRequest
php artisan test --testsuite=Feature
php artisan test --testsuite=Unit
vendor/bin/phpunit --group=lai
```

Base de dados de teste: PostgreSQL (o módulo usa `EXTRACT(DAY FROM ...)`,
`to_char` e `::timestamp with time zone`, incompatíveis com SQLite/MySQL).

## 2. Fixtures e factories necessárias

```php
// database/factories/InformationRequestFactory.php
InformationRequest::factory()
    ->for(Person::factory()->pf(), 'person')
    ->for(Status::factory()->state(['id' => StatusLai::EmTramitacao]), 'status')
    ->sequence(
        ['protocol' => '20250101120000', 'created_at' => now()],
        ['protocol' => '20250201120000', 'created_at' => now()->subDays(25)],
    );
```

Factories a criar:

| Factory | Campos mínimos |
|---|---|
| `InformationRequestFactory` | `request`, `mask_request`, `protocol` (único), `ip`, `browser`, `persons_id`, `status_id`, `created_at` |
| `PersonFactory` | `name`, `cpf`, `rg` (PF) / `cnpj`, `social_name` (PJ), `email`, `phone` |
| `AddressFactory` | `persons_id`, `street_name`, `street_number`, `zip_code`, `neighborhood`, `cities_id` |
| `EmailFactory` | `persons_id`, `email_address` |
| `PhoneFactory` | `persons_id`, `phone_number` |
| `AnswerFactory` | `informations_request_id`, `answer` |
| `ArchiveFactory` | `name`, `path`, `extension`, `description` |

Seed de ambiente (view/seed para o teste de interface):

```php
// tests/Seed/LaiStatusSeeder.php
Status::firstOrCreate(['id' => 1], ['name' => 'Em Tramitação', 'status_category_id' => 1]);
Status::firstOrCreate(['id' => 2], ['name' => 'Atendido',      'status_category_id' => 1]);
Status::firstOrCreate(['id' => 3], ['name' => 'Indeferido',    'status_category_id' => 1]);
Status::firstOrCreate(['id' => 4], ['name' => 'Prorrogado',    'status_category_id' => 1]);
```

Além disso: `configurations` com `email`, `email_lai`, `titulo_site`,
`layout` e `lai.deadline_days = 20`; `modulos` com o slug `solicitacoes-lai`;
`resources` com os slugs `lai` e `respostas-lai`; `menu_orders` com o slug
`lai`; e um usuário Sentinel com as permissões `lai.index`, `lai.edit`,
`lai.show`, `respostas-lai.*`.

## 3. Casos de teste — Portal (cidadão)

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | Formulário válido com PF, 25 caracteres de pedido, e-mail válido, reCAPTCHA válido | `POST /informacao-solicitada` | 302/200; 1 registro em `informations_requests` com `status_id = 1`; `persons`, `addresses`, `emails` e `phones` criados; `protocol` com 14 dígitos; `mask_request` não nulo; `ip` e `browser` preenchidos |
| P2 | idem, com PJ (`cnpj` + `social_name`) | `POST /informacao-solicitada` | `persons.cnpj` salvo sem pontuação e em maiúsculas; `social_name` gravado |
| P3 | pedido com menos de 20 caracteres | `POST /informacao-solicitada` | 422; mensagem "Descreva melhor sua solicitação."; nenhum registro criado |
| P4 | CPF inválido | `POST /informacao-solicitada` | 422 com "Por favor, informe um CPF válido." |
| P5 | CNPJ inválido | `POST /informacao-solicitada` | 422 com "Por favor, informe um CNPJ válido." |
| P6 | e-mail ausente | `POST /informacao-solicitada` | 422 com "Por favor, informe seu email." |
| P7 | reCAPTCHA ausente | `POST /informacao-solicitada` | 422 com "Por favor, marque a opção não sou um robô" |
| P8 | protocolo já existente | `POST /informacao-solicitada` | 422 com "Sua solicitação já foi recebida. Consulte o seu protocolo." |
| P9 | pessoa sem `person-type` | `POST /informacao-solicitada` | 422 com "Não foi possível recuperar o tipo de pessoa" |
| P10 | pedido contendo CPF, telefone, e-mail e nome | `POST /informacao-solicitada` | `request` mantém o texto original; `mask_request` não contém o CPF em claro, o telefone nem o e-mail |
| P11 | `gemini_api_key` vazia | `POST /informacao-solicitada` | `mask_request` é gerado pela camada determinística; nenhum erro 500 |
| P12 | API de IA fora do ar (mock HTTP 500) | `POST /informacao-solicitada` | `mask_request` = resultado determinístico; a solicitação é persistida |
| P13 | solicitação existente com protocolo X | `POST cms/ajax/get/information/by/protocol` | 200; **o JSON não contém** `request` em claro, `ip`, `browser` nem os dados do titular (ver `05-REGRAS-E-PORTABILIDADE.md`, item 3.3) |
| P14 | protocolo inexistente | `POST cms/ajax/get/information/by/protocol` | 404 e corpo sem mensagem de erro interno (hoje a v6 devolve 500 com stack trace) |
| P15 | protocolo inexistente | `POST cms/ajax/get/answer/by/information` | 404 controlado, sem exceção (hoje a v6 devolve 500) |
| P16 | solicitação com 1 resposta e 1 anexo | `POST cms/ajax/get/answer/by/information` | 200; array com `answer` e `view` renderizado; o link do arquivo responde 200 |
| P17 | 30 solicitações em anos diferentes | `GET /relatorio-estatistico?ano=2025` | view renderiza; os 8 indicadores de prazo são os esperados; gráfico com 8 fatias |
| P18 | `full_list_requests_disabled = 1` | `GET /relatorio-estatistico?ano=2025` | o botão "Listagem completa das solicitações" **não** aparece |
| P19 | 5 solicitações | `GET /relatorio-estatistico?ano=2025&type=xls` | arquivo `.xlsx` baixado com 4 colunas (Status, Protocolo, Solicitante, Data) e protocolo/nome **mascarados** |
| P20 | idem com `type=txt` | idem | arquivo `.txt` com `masked_protocol` e `masked_name` |

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

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | 1 solicitação em tramitação | `GET cms/solicitacoes/lai/1` | 200; view renderizada; a tabela tem o id `tabela_em-tramitacao`; log de atividade registrado |
| C2 | idem | `GET cms/solicitacoes/lai/2` | 200; id `tabela_atendidos` |
| C3 | idem | `GET cms/solicitacoes/lai/3` | 200; id `tabela_indeferidos` |
| C4 | idem | `GET cms/solicitacoes/lai/4` | 200; id `tabela_prorrogados` |
| C5 | 5 solicitações em tramitação | `GET cms/datatable/em-tramitacao` | JSON DataTable com 5 linhas; colunas `value`, `status`, `solicitante`, `protocolo`, `criado_em` |
| C6 | usuário sem a permissão `lai.edit` | `GET cms/datatable/em-tramitacao` | a flag `edit` das linhas é `false`; os botões de ação não são renderizados |
| C7 | 1 solicitação | `GET cms/respostas-lai/{id}` | 200; mostra IP, navegador, protocolo, status e os dados do titular |
| C8 | 1 solicitação | `POST cms/answer` com `type=1` e `answer` preenchido | 1 registro em `answers`; `status_id` passa a 2; e-mail `email_answer_no_attach` enviado ao solicitante; redirect para `info-request-by-status` com 2 |
| C9 | 1 solicitação + 1 arquivo | `POST cms/answer` com `type=1` e `archive[0][0]` | arquivo gravado em `storage/app/public/answer/{id}/`; `archives` criado; pivot `answer_archives` com 1 linha; e-mail `email_answer_attach` com anexo |
| C10 | 1 solicitação | `POST cms/answer` com `type=0` e `rejected` preenchido | `status_id` passa a 3; e-mail `email_rejected` enviado |
| C11 | 1 solicitação | `POST cms/answer` sem `answer` | 422 com "O campo resposta é obrigatório."; status inalterado |
| C12 | 1 solicitação em tramitação | `GET cms/solicitacoes/{id}/lai/4` | `status_id = 4`; e-mail `email_to_extend` enviado; log registrado |
| C13 | 1 solicitação já atendida | `GET cms/solicitacoes/{id}/lai/4` | **deve ser rejeitado** (hoje a v6 permite) |
| C14 | 1 solicitação | `GET cms/solicitacoes/{id}/lai/99` | **deve ser rejeitado** (hoje a v6 grava 99) |
| C15 | usuário logado sem permissão de LAI | `GET cms/solicitacoes/{id}/lai/4` | 403 / redirect de aviso (hoje a v6 permite — rota em `openRoutes`) |
| C16 | 1 solicitação | `GET cms/solicitacoes/lai/1` sem login | redirect para `dashboard.index` |
| C17 | 1 solicitação | `POST cms/answer` com arquivo `.exe` | 422 (o legado aceita qualquer tipo) |
| C18 | 1 solicitação | `GET cms/respostas-lai` (índice do resource) | **deve** renderizar (hoje a v6 falha por falta de `$status`) |

## 5. Casos de teste — Unitários

| # | Alvo | Entrada | Assert |
|---|---|---|---|
| U1 | `DataMaskTrait::maskTextDeterministic` | "Meu CPF 123.456.789-00 e o telefone (83) 98765-4321, João Silva" | CPF e telefone mascarados; telefone e e-mail removidos |
| U2 | `maskTextDeterministic` | texto com "Rua das Acácias, 234" e "São Paulo - SP" | endereço e cidade mascarados |
| U3 | `maskProtocol` | "20251021120000" | "2025\*\*\*\*\*\*0000" |
| U4 | `maskName` | "João da Silva" | primeiro nome visível, demais com asterisco |
| U5 | `maskName` | "" | "Anônimo" |
| U6 | `maskName` | qualquer texto + modalidade "Anônimo" | "Anônimo" |
| U7 | `maskName` | qualquer texto + modalidade "Sigiloso" | "Sigiloso" |
| U8 | cálculo de prazo | solicitação criada há 10 dias | "dentro do prazo" |
| U9 | cálculo de prazo | solicitação criada há 25 dias | "fora do prazo" |
| U10 | cálculo de prazo | solicitação prorrogada há 3 dias (`updated_at`) | "dentro do prazo" (base = `updated_at`) |
| U11 | gerador de protocolo | 1000 chamadas em 1 segundo | 1000 protocolos distintos (o `date('YmdHis')` do legado colide) |
| U12 | `enum StatusLai` | — | `from(1) === EmTramitacao`, etc. |

## 6. Testes de integração de banco

| # | Alvo | Assert |
|---|---|---|
| B1 | `scope` de prazo (equivalente a `getByBetweenDate`) | a consulta **executa sem erro** no PostgreSQL (o `groupBy('status_id')` sobre `select *` da v6 falha) |
| B2 | `scope` de prazo com resposta | retorna apenas as solicitações cuja resposta foi registrada após 20 dias |
| B3 | filtro de ano para status 4 | usa `created_at` (na v5 usava `updated_at`) |
| B4 | FK `informations_requests.persons_id` | não permite órfão; `on delete` definido |
| B5 | índice único de `protocol` | impede duplicidade no banco, não só na aplicação |
| B6 | exclusão lógica | `deleted_at` preenchido; a solicitação não aparece nas listagens nem nas 4 vistas |

## 7. Testes de interface (Playwright / Dusk)

| Cenário | Passos | Assert |
|---|---|---|
| E1 — Registrar solicitação | abrir "Solicitar Informação", preencher PF, submeter | mensagem de sucesso, protocolo exibido, 1 registro no banco |
| E2 — Alternância PF/PJ | marcar "Pessoa Jurídica" | `#cpf`, `#name`, `#rg` somem; `#cnpj`, `#social_name`, `#fantasy_name` aparecem |
| E3 — Acompanhar | informar o protocolo | tabela de status com a cor correta; respostas listadas |
| E4 — Consultar protocolo inexistente | informar protocolo inválido | "Não foi possível encontrar o pedido solicitado." |
| E5 — Fluxo do operador | entrar no CMS, menu LAI → Em Tramitação, responder com anexo | status vira "Atendido"; linha some da lista de em tramitação e aparece na de atendidos; e-mail recebido no MailHog |
| E6 — Prorrogar | clicar em "Prorrogar" | status vira "Prorrogado"; e-mail "PRORROGADO: Lei de Acesso à Informação [...]" |
| E7 — Relatório estatístico | filtrar ano, exportar xls | arquivo baixado com 4 colunas e dados mascarados |

## 8. Matriz de cobertura mínima aceitável

| Área | Cobertura mínima |
|---|---|
| `Portal\InformationController` (store, storeRequestInformacaoAjax, getInformationByProtocol, getAnswerByInformation) | 100% das regras de validação e dos caminhos de erro |
| `Prefecture\InformationsRequestController` (listByStatus, statusUpdate, show) | 100% dos ramos |
| `Prefecture\AnswerController@store` | 100% (com/sem anexo, type 0/1, e-mail falhando) |
| `DataMaskTrait` | ≥ 90% (todas as regex) |
| Model `InformationsRequest` (scopes, accessors, relações) | 100% |
| Consultas de relatório e exportações | 100% dos formatos |
| Regressão de segurança (o que não pode ser exposto) | 100% — teste dedicado e bloqueante no CI |
