# Ouvidoria — Testes

## 1. Estratégia

O módulo **não possui testes** no legado (verificado: `v6/tests/Feature/` e `v6/tests/Unit/` cobrem apenas o Dashboard e o módulo de Revisões; não há arquivo relacionado a Ouvidoria).

Estratégia proposta para a v7:

| Camada | Tipo | Foco |
|---|---|---|
| Unit | Modelo, enums e escopos | `OmbudsmanStatus`, `OmbudsmanModality`, escopo de prazo, accessors de mascaramento, geração de protocolo |
| Feature (Portal) | Rotas públicas | Criação de manifestação, consulta por protocolo, contestação, relatório estatístico, exportações |
| Feature (CMS) | Rotas autenticadas | Autorização, listagem, resposta, indeferimento, prorrogação, encaminhamento, CRUDs de apoio |
| Contrato de API | Resources | Formato JSON estável e **ausência** de dados pessoais no escopo público |
| E-mail | Testes com `Mail::fake()` | Destinatários, templates escolhidos, variáveis passadas |
| LGPD | Testes de mascaramento | `mask_description`, `mask_answer`, `masked_protocol`, `masked_name` |
| Integração (PDF/planilha) | Testes do export | Estrutura do arquivo gerado |

### 1.1 Ambiente de teste

`v6/phpunit.xml` define `APP_ENV=testing`, `CACHE_DRIVER=array`, `SESSION_DRIVER=array`, `QUEUE_DRIVER=sync`.

**Ajustes necessários na v7:**
- `QUEUE_CONNECTION=sync` → `database` (ou `Bus::fake()`) para testar os jobs de e-mail e mascaramento.
- `MAIL_DRIVER=array` (padrão do Laravel 5.5) — usar `Mail::fake()`.
- O banco deve ser **PostgreSQL** (as views SQL e o `EXTRACT`/`ilike` são Postgres-específicos).
- `recaptcha` deve ser **desativado** em testes (`RECAPTCHA_ENABLED=false`) ou o driver de teste do pacote deve ser usado, sob pena de falha em toda criação de manifestação.

### 1.2 Fixtures necessárias

Nenhuma factory do módulo existe. Criar:

| Factory | Campos |
|---|---|
| `OmbudsmanRequestFactory` | `protocol` único, `type_id`, `subject_id`, `response_method_id`, `origin_id`, `status_id=1`, `modality='identificado'`, `description`, `ip`, `browser`, `token` |
| `OmbudsmanAnswerFactory` | `ombudsman_request_id`, `answer`, `mask_answer` |
| `OmbudsmanSubjectFactory` | `title`, `sector_id`, `email`, `responsible` |
| `OmbudsmanSectorFactory` | `title`, `content_id`, `active=true` |
| `OmbudsmanManifestationTypeFactory` | `title`, `status=true` |
| `OmbudsmanOriginFactory` | `title` |
| `OmbudsmanResponseMethodFactory` | `title` |
| `OmbudsmanCannedResponseFactory` | `title`, `message` |
| `OmbudsmanRequestLogFactory` | `ombudsman_request_id`, `user_id`, `type`, `observation` |

Além disso, uma factory de `Status` (com os 4 status usados) e de `Content` da categoria "Secretarias".

### 1.3 Seed de ambiente de teste

| Given | Valor |
|---|---|
| Setores | 3 (Saúde, Educação, Obras) |
| Assuntos | 1 por setor, com `email` e `responsible` |
| Tipos | 6 (Denúncia, Reclamação, Solicitação, Sugestão, Elogio, Pedido de Informação) |
| Origens | 6 (Telefone, Site, Presencial, E-mail, Aplicativo Móvel, Redes Sociais) |
| Formas de resposta | 4 (E-mail, Telefone, Correspondência, Não deseja resposta) |
| Respostas prontas | 3 |
| Manifestações | 12 espalhadas em 2 anos e 4 status, para exercitar o relatório |

## 2. Como rodar

```bash
# v6 (legado)
cd v6 && ./vendor/bin/phpunit

# v7 (proposto)
php artisan test --testsuite=Feature
php artisan test --filter=Ombudsman
php artisan test --filter=Ouvidoria
```

## 3. Casos de teste — Portal

### 3.1 Criação de manifestação

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | payload completo válido | POST `/ouvidoria/store` | 302/200; 1 linha em `ombudsman_requests`; `status_id=1`; `protocol` casa com `/^OUV-\d{8}-\d{6}$/` |
| P2 | payload sem `description` | POST | 422; erro em `description` |
| P3 | payload sem `token` | POST | 422; erro em `token` |
| P4 | payload sem `modality` | POST | 422 |
| P5 | payload com `type_id` inexistente | POST | 422 |
| P6 | payload com `modality='anonimo'` | POST | `anonymous=true`; `name`/`email`/`phone` nulos |
| P7 | payload com `modality='sigiloso'` | POST | `anonymous=false`; dados pessoais gravados |
| P8 | payload com `description` contendo CPF `123.456.789-00` | POST | `mask_description` **não** contém o CPF em claro |
| P9 | payload com `description` contendo e-mail e telefone | POST | `mask_description` com e-mail e telefone mascarados |
| P10 | 2 payloads no mesmo segundo | POST x2 | o 2º **falha** (colisão de protocolo) — teste de caracterização do bug R10 |
| P11 | payload válido | POST | 2 e-mails enviados: um ao cidadão (se houver e-mail) e um ao setor (`OmbudsmanSubject.email`) |
| P12 | payload sem `email` | POST | apenas 1 e-mail (o do setor) |
| P13 | payload válido | POST | a view de retorno mostra o protocolo e o prazo (`configurations.response_time`) |
| P14 | página estática `ouvidoria` inexistente, mas `contato` existente | GET `/ouvidoria` | renderiza com o conteúdo de `contato` |
| P15 | nenhuma das duas páginas estáticas | GET `/ouvidoria` | view `admin.errors.conteudo-nao-encontrado` |

### 3.2 Consulta por protocolo

| # | Arrange | Act | Assert |
|---|---|---|---|
| P16 | chamado com protocolo `OUV-01012026-120000` | POST `ajax.get.ombudsman.by.protocol` | 200; JSON com `protocol` e `status_id` |
| P17 | protocolo inexistente | POST | 200 com `{"message": "Protocolo não encontrado."}` |
| P18 | chamado com 2 respostas | POST `ajax.get.answer.by.ombudsman` | JSON com 2 itens, cada um com `mask_answer` (não `answer`) |
| P19 | chamado sem respostas | POST `ajax.get.answer.by.ombudsman` | HTTP 404 |
| P20 | chamado com 1 resposta | POST `get.ombudsman.answers` | JSON `{request: {protocol, mask_description, status_id}, answers: [...]}` |
| P21 | chamado inexistente | POST `get.ombudsman.answers` | HTTP 204 com corpo `'Ocorreu um erro.'` |

> **Teste de caracterização importante:** em P16/P18, o payload **atual** inclui `name`, `email`, `phone`, `ip`, `browser` e `token` em claro. Registrar esse comportamento como **falha** a ser corrigida na v7.

### 3.3 Contestação

| # | Arrange | Act | Assert |
|---|---|---|---|
| P22 | chamado com `token = Hash::make('segredo')` | POST `new.message.ombudsman` com `contestation_token='segredo'` | 200; `answer_contestation` gravado; `answer_contestation_date` preenchido |
| P23 | idem | POST com `contestation_token='errado'` | 403; `{"error": "Token inválido."}`; nada gravado |
| P24 | chamado já com `answer_contestation` | POST válido | grava em `answer_contestation_2` |
| P25 | chamado com 2 contestações | POST válido | **sobrescreve** a 1ª (comportamento atual — teste de caracterização) |

### 3.4 Relatório estatístico

| # | Arrange | Act | Assert |
|---|---|---|---|
| P26 | 4 chamados em tramitação, 2 criados há 10 dias, 2 há 30 dias | GET `?ano=2026` | `emTramitacao_no_prazo=2`, `emTramitacao_apos_prazo=2` |
| P27 | 2 chamados atendidos, respostas em 5 e 25 dias | GET `?ano=2026` | `atendido_no_prazo=1`, `atendido_apos_prazo=1` |
| P28 | idem, mas com resposta em exatamente 20 dias | GET `?ano=2026` | conta como **no prazo** (`<= 20`) |
| P29 | idem, mas com resposta em 21 dias | GET `?ano=2026` | conta como **após o prazo** |
| P30 | chamado criado em outro ano | GET `?ano=2026` | não entra na contagem |
| P31 | chamado com `deleted_at` preenchido | GET `?ano=2026` | **hoje entra** na contagem (bug R12) — teste de caracterização |
| P32 | chamado prorrogado (`status_id=4`) | GET `?ano=2026` | medido por `updated_at`, não por `created_at` (comportamento atual) |
| P33 | — | GET `?ano=2026&type=xls` | download `.xlsx` |
| P34 | — | GET `?ano=2026&type=csv` | download `.csv` |
| P35 | — | GET `?ano=2026&type=pdf` | download `.pdf` |
| P36 | — | GET `?ano=2026&type=txt` | download `.txt`; conteúdo com o cabeçalho "Ouvidoria - Relatório Estatístico" |
| P37 | — | GET `?ano=2026&type=xls` | o conteúdo exportado usa `masked_protocol` e `masked_name` — **não** o protocolo em claro |

### 3.5 Mascaramento (LGPD)

| # | Entrada | Esperado em `mask_description` |
|---|---|---|
| M1 | `Meu CPF é 123.456.789-00` | `123.\***.\***-00` |
| M2 | `CNPJ 12.345.678/0001-90` | `12.\***.\***/****-90` |
| M3 | `Tel (11) 98765-4321` | dígitos mascarados |
| M4 | `Fale comigo em joao@exemplo.com.br` | `*******@exemplo.com.br` |
| M5 | `Nasci em 10/05/1980` | `********` |
| M6 | `uso.sistema#1234` | tudo `*` |
| M7 | `CID-10: F41.1` | `CID-10: *****` |
| M8 | `meu nome é João da Silva` | nome substituído pelo placeholder |

| # | Cenário | Esperado |
|---|---|---|
| M9 | `masked_protocol` | protocolo parcialmente mascarado |
| M10 | `masked_name` com `anonymous=true` | `'Anônimo'` |
| M11 | `masked_name` com `modality='sigiloso'` | nome mascarado |
| M12 | `masked_name` com `modality='identificado'` | nome mascarado (padrão do trait) |

## 4. Casos de teste — CMS

### 4.1 Autorização

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | usuário anônimo | GET `/cms/chamados-da-ouvidoria` | redirect para `login` |
| C2 | usuário sem `ouvidoria.index` | GET `/cms/chamados-da-ouvidoria` | redirect + flash de aviso |
| C3 | `super-admin` | GET `/cms/chamados-da-ouvidoria` | 200 |
| C4 | usuário sem `ouvidoria.edit` | GET `/cms/chamado-da-ouvidoria/responder/{id}` | bloqueado (o middleware exige `ouvidoria.edit`) |
| C5 | usuário com `ouvidoria.edit` | GET `/cms/chamado-da-ouvidoria/responder/{id}` | 200 |

> **Teste de caracterização:** em C2, se o `check.route.permission` não for registrado no controller (por causa do `__construct` sobrescrito), o usuário **entra** mesmo sem permissão. Registrar o comportamento atual.

### 4.2 Resposta e indeferimento

| # | Arrange | Act | Assert |
|---|---|---|---|
| C6 | chamado em tramitação | POST `/cms/chamado-da-ouvidoria/responder` com `type=1`, `answer='texto'` | 1 linha em `ombudsman_answers`; `status_id=2`; `mask_answer` gravado |
| C7 | idem | POST com `type=0`, `rejected='justificativa'` | `answer` = justificativa; `status_id=3` |
| C8 | — | POST sem `answer` e `type=1` | 422 (Form Request) |
| C9 | — | POST sem `rejected` e `type=0` | 422 |
| C10 | chamado com resposta | GET `/cms/chamado-da-ouvidoria/responder/{id}` | 200; exibe as respostas anteriores e o histórico de tramitação |
| C11 | chamado com `subject_from_id` nulo em um log | GET `/cms/chamado-da-ouvidoria/responder/{id}` | **erro fatal** (acesso a `null->title`) — teste de caracterização do bug do blade |
| C12 | resposta enviada | — | e-mail `email_answer_no_attach` enviado ao cidadão |
| C13 | resposta **com** anexo | POST com `archive[]` | **`BadMethodCallException`/`QueryException`** — `archives()` comentado + pivô inexistente (teste de caracterização do bug R1) |
| C14 | qualquer falha na transação | — | hoje executa `dd($e)` — na v7 deve exibir mensagem amigável |

### 4.3 Prorrogação

| # | Arrange | Act | Assert |
|---|---|---|---|
| C15 | chamado em tramitação | GET `/cms/chamado-da-ouvidoria/prorrogar/{id}` | `status_id=4`; e-mail `email_to_extend` enviado; flash success |
| C16 | chamado sem e-mail | GET | status alterado; **nenhum** e-mail enviado (guarda `if (!empty($to))`) |
| C17 | id inexistente | GET | exceção não capturada → 500 |

### 4.4 Encaminhamento

| # | Arrange | Act | Assert |
|---|---|---|---|
| C18 | chamado com assunto A | POST `/cms/chamado-da-ouvidoria/enviar` com `title_subject='B'` | `subject_id` alterado para o assunto B; 1 log `type='forward'` |
| C19 | idem | — | `sector_from_id` e `sector_to_id` gravados; `subject_from_id`/`subject_to_id` gravados; `observation` preenchida |
| C20 | `title_subject` inexistente | POST | `subject_id` **inalterado**; log gravado com `subject_to_id` = `subject_id` atual |
| C21 | idem | POST | e-mail `email_admin` (ombudsman-send-requests) enviado para `new_email` |

### 4.5 Atualização de chamado (bug R2)

| # | Arrange | Act | Assert |
|---|---|---|---|
| C22 | 2 chamados (A e B) | `PUT /cms/chamados-da-ouvidoria/{id_de_B}` com `{"description": "X"}` | **hoje**: o chamado **A** (primeiro registro) é alterado e B fica intacto — teste de caracterização; na v7: apenas B é alterado |
| C23 | — | PUT com `{"status_id": 99}` | **hoje** sobrescreve `status_id` de qualquer forma (`$request->all()`) — teste de caracterização; na v7: campo não mass-assignable |

### 4.6 CRUDs de apoio

| # | Cenário | Assert |
|---|---|---|
| C24 | POST `/cms/assuntos` sem `sector_id` | 422 |
| C25 | POST `/cms/assuntos` sem `email` | 422 |
| C26 | POST `/cms/assuntos` válido | criado; `subjects` aparece no formulário do portal |
| C27 | POST `/cms/setores-orgaos` válido | criado |
| C28 | DELETE setor **com** assuntos vinculados | flash de erro; setor **não** excluído |
| C29 | DELETE setor **sem** assuntos | excluído (soft delete) |
| C30 | POST `ajax.setores-orgaos.deleteAll` com setor bloqueado | HTTP 422 com a lista dos bloqueados |
| C31 | POST `/cms/tipos-de-chamado/set-status` | `status` alternado |
| C32 | GET `/cms/respostas-prontas/api` | JSON com todas as respostas prontas |
| C33 | POST `/cms/origens-de-chamado` com `description` | **hoje**: erro de coluna inexistente se o campo for enviado — teste de caracterização (bug §4.5 das regras) |

### 4.7 DataTables

| # | Cenário | Assert |
|---|---|---|
| C34 | GET `/cms/datatable/chamados-da-ouvidoria` | JSON com as colunas da view `chamados_ouvidoria` |
| C35 | idem | **hoje** inclui `protocolo` e `descricao_resumida` em claro — teste de caracterização (LGPD) |
| C36 | GET `/cms/datatable/assuntos` | JSON com `titulo_assunto` e `secretaria_vinculada` |
| C37 | GET `/cms/datatable/setores-orgaos` | JSON com `titulo` e `status` ('Ativo'/'Inativo') |
| C38 | GET `/cms/datatable/{tipos-de-chamado,formas-de-resposta,origens-de-chamado,respostas-prontas}` | JSON com as colunas de cada view |

## 5. Testes de API Resource (v7)

| # | Cenário | Assert |
|---|---|---|
| R1 | `OmbudsmanRequestResource` | **nunca** contém `name`, `email`, `phone`, `ip`, `browser`, `token` |
| R2 | idem | contém `protocol` mascarado, `solicitante` mascarado, `descricao` mascarada |
| R3 | `OmbudsmanAnswerResource` | contém `mask_answer`, **não** `answer` |
| R4 | `OmbudsmanRequestFullResource` (protegido) | contém os dados completos |
| R5 | Recurso de estatísticas | contém apenas contadores agregados, sem nenhum dado pessoal |
| R6 | Recurso de log de tramitação | contém `type`, `observation`, `created_at`, nome do usuário |

## 6. Testes de e-mail

| # | Cenário | Assert |
|---|---|---|
| E1 | Envio pelo portal | `Mail::assertSent` 2× (cidadão + setor), com os templates corretos |
| E2 | Resposta sem anexo | template `email_answer_no_attach` |
| E3 | Resposta com anexo | template `email_answer_attach` (hoje inalcançável — bug R1) |
| E4 | Indeferimento | template `email_rejected` |
| E5 | Prorrogação | template `email_to_extend` |
| E6 | Encaminhamento | template `prefecture.emails.ombudsman-send-requests.email_admin` |
| E7 | Todos | o assunto do e-mail contém o protocolo entre colchetes: `"[OUV-...]"` |
| E8 | Todos | a saudação segue a hora do servidor ("Bom dia"/"Boa tarde"/"Boa noite") |
| E9 | E-mail ao setor | **não** contém `ip` nem `browser` do cidadão (na v7) |

## 7. Testes de regressão dos gaps conhecidos

Documentam o comportamento atual e devem ser atualizados quando a v7 corrigir o gap:

| # | Gap | Teste de caracterização |
|---|---|---|
| G1 | `archives()` comentado + pivô inexistente | responder com anexo **falha** com exceção |
| G2 | `update()` altera o primeiro registro | `PUT` no id B altera o registro A |
| G3 | 7 controllers sem `check.route.permission` | usuário sem permissão **entra** no CRUD de assuntos |
| G4 | `modality` validada como `integer` | POST com `modality='identificado'` é **rejeitado** pela validação |
| G5 | captcha não validado | POST sem `g-recaptcha-response` é **aceito** |
| G6 | `modality == 'Anônimo'` nunca casa | POST com `modality='anonimo'` e sem checkbox `anonymous` → `anonymous=false` |
| G7 | `OmbudsmanRequest` sem SoftDeletes | registro com `deleted_at` preenchido **aparece** em `OmbudsmanRequest::all()` e no relatório |
| G8 | `dd($e)` em produção | erro na resposta mostra o dump da exceção |
| G9 | `sector_from_id` com id de setor | log gravado com valor de `ombudsman_sectors.id` em coluna com FK para `contents` |
| G10 | `OmbudsmanOrigin` com `description` no fillable | gravar `description` causa erro de coluna inexistente |
| G11 | Contato de `$log->subjectFrom->title` sem `optional()` | renderizar a tela de resposta com log sem `subject_from_id` causa erro fatal |
| G12 | Consulta por protocolo expõe dados em claro | JSON contém `name`, `email`, `ip`, `browser`, `token` |

## 8. Checklist de cobertura

| Área | Coberto |
|---|---|
| Criação de manifestação (portal) | P1–P15 |
| Consulta por protocolo | P16–P21 |
| Contestação | P22–P25 |
| Relatório estatístico e exportações | P26–P37 |
| Mascaramento LGPD | M1–M12 |
| Autorização (CMS) | C1–C5 |
| Resposta / indeferimento | C6–C14 |
| Prorrogação | C15–C17 |
| Encaminhamento | C18–C21 |
| Atualização de chamado | C22–C23 |
| CRUDs de apoio | C24–C33 |
| DataTables | C34–C38 |
| API Resources | R1–R6 |
| E-mails | E1–E9 |
| Regressão de gaps | G1–G12 |
