# Newsletters — Estratégia de Testes

> **Módulo:** `newsletters` (tabela `modulos`, id 53)
> **Referência:** a v6 **não possui testes** para este módulo (nenhum arquivo em
> `v6/tests/` referencia `Newsletters`, `NewsLetterController` ou a rota
> `newsletter.storeportal` — verificado por busca no repositório).

---

## 1. Estratégia

| Tipo | Ferramenta sugerida (v7) | Escopo |
|---|---|---|
| **Feature** (HTTP) | PHPUnit + `RefreshDatabase` | rotas do portal e do CMS: criação, validação, edição, exclusão, exportação |
| **Unit** | PHPUnit | regras de domínio (normalização de e-mail, escape de CSV) |
| **Browser** (opcional) | Laravel Dusk / Playwright | presença do formulário no portal conforme os dois interruptores |

Regras gerais:

- Usar `RefreshDatabase` e o schema de migrações da v7 (não as views SQL do legado).
- Criar os usuários do Sentinel com as permissões `newsletter.index`, `.create`,
  `.edit`, `.update`, `.delete`, `.export` para exercitar a policy.
- Para o endpoint público, garantir que o usuário **não** está autenticado — é o
  caminho mais regressivo do módulo.

## 2. Fixtures / factories necessárias

```php
// database/factories/NewsletterFactory.php
class NewsletterFactory extends Factory
{
    protected $model = Newsletter::class;

    public function definition(): array
    {
        return [
            'name'       => fake()->name(),
            'email'      => fake()->unique()->safeEmail(),
            'ip_address' => fake()->ipv4(),
            'sent_at'    => now(),
            'active'     => true,
        ];
    }

    public function inativo(): static { return $this->state(fn () => ['active' => false]); }
    public function excluido(): static { return $this->state(fn () => ['deleted_at' => now()]); }
}
```

| Factory | Uso |
|---|---|
| `NewsletterFactory` | assinaturas ativas, inativas e em soft delete |
| Usuário Sentinel com permissões | `super-admin` e um usuário comum sem permissões |

## 3. Casos de teste — portal público

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Assinatura válida | POST `/newsletter` com `name`, `email`, `active=true` | — | 201/302; 1 linha em `newsletters` com `ip_address` e `sent_at` preenchidos; `active = true` |
| P2 | E-mail duplicado | 1 assinatura com `fulano@example.com` | POST com o mesmo e-mail | 422; mensagem "E-mail já cadastrado."; nenhuma linha nova |
| P3 | E-mail inválido | — | POST com `email = "invalido"` | 422; erro no campo `email` |
| P4 | E-mail ausente | — | POST sem `email` | 422; erro "O campo email é obrigatório." |
| P5 | `active` ausente | — | POST sem `active` | **não** deve gerar 500 (bug da v6); a linha é criada com `active = false` |
| P6 | `name` ausente | — | POST sem `name` | aceito (coluna anulável) ou 422 conforme a regra escolhida; nunca 500 |
| P7 | E-mail de assinatura já excluída | assinatura com `deleted_at` preenchido | POST com o mesmo e-mail | comportamento documentado: na v6 é **rejeitado** (UNIQUE global) |
| P8 | CSRF | POST sem token | — | 419 |
| P9 | Rate limit | 11 requisições seguidas | — | 429 a partir da 11ª (se o limit for adotado) |

## 4. Casos de teste — CMS

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | Listagem | 3 assinaturas (1 excluída) | GET `/cms/newsletter` | 200; a view renderiza o esqueleto da tabela; a collection não é carregada inutilmente (assert de query count) |
| C2 | DataTable | 3 assinaturas, 1 excluída | GET `/cms/datatable/newsletter` | JSON com 2 linhas; campo `value` sequencial; campos `nome`, `email`, `ip`, `enviado`, `ativo` |
| C3 | Cadastro válido | usuário com `newsletter.create` | POST `/cms/newsletter/store` | 302 para `newsletter.index`; flash `success`; linha criada |
| C4 | Cadastro sem `name` | — | POST `/cms/newsletter/store` | 422; erro no campo `name` |
| C5 | Cadastro com `name` > 255 | — | POST | 422 |
| C6 | Atualização com o mesmo e-mail | assinatura id 1 | PUT `/cms/newsletter/update/1` com o mesmo e-mail | 302 e **sem** erro de unicidade (o id atual é ignorado) |
| C7 | Atualização com e-mail de outro registro | 2 assinaturas | PUT da 1ª com o e-mail da 2ª | 422 "E-mail já cadastrado." |
| C8 | Exclusão | assinatura existente | DELETE `/cms/newsletter/delete/{id}` | 200 `{success}`; `deleted_at` preenchido; a linha continua na tabela |
| C9 | Exclusão de id inexistente | — | DELETE `/cms/newsletter/delete/999999` | **404** na v7 (a v6 devolve 500 `{erro}`) |
| C10 | Exportação | 2 assinaturas, 1 excluída | GET `/cms/newsletter/export` | CSV com BOM UTF-8; cabeçalho correto; **apenas** as assinaturas não excluídas (correção do bug da v6) |
| C11 | Exportação vazia | base vazia | GET `/cms/newsletter/export` | redirect com mensagem "Nenhuma inscrição encontrada para exportar." |
| C12 | Escape de fórmula | assinatura com `name = "=HYPERLINK(\"http://x\")"` | GET export | o valor sai prefixado com `'` ou neutralizado |
| C13 | Permissões | usuário sem `newsletter.index` | GET `/cms/newsletter` | 403/redirect; item de menu ausente |
| C14 | Permissões | usuário sem `newsletter.delete` | GET `/cms/datatable/newsletter` | campo `delete = false`; botão de exclusão oculto |

## 5. Fixtures de view / seed para o ambiente de teste

| Item | Origem | Finalidade |
|---|---|---|
| `configurations` com `key = 'newsletters'` e `value = 0` | `v6/database/seeds/UpdateConfigurationOrderSeederNewsletter.php` | garante que `getStatusDisplayNewsletters()` não receba array vazio |
| `modulos` com `slug = 'newsletters'` e `status = 1` | migration `2024_11_01_150206_create_modulos_table.php` (id 53) | habilita o formulário no portal e o item no menu |
| `menu_orders` com `slug = 'newsletter'` e `modulo_id = 53` | migration `2024_10_30_161709_inserting_menu_orders_newsletter.php` | habilita o item de menu do CMS |
| Layout ativo | configuração `layout` | define qual `portal.layouts.*` é renderizado nos testes de view |

> Atenção: `getStatusDisplayNewsletters()` acessa `$result[0]->value` sem verificar se a
> lista está vazia. Sem a seed de configuração, a view do portal estoura com
> "Undefined offset" — o seed é **obrigatório** no ambiente de teste.

## 6. Testes de view do portal

| # | Cenário | Assert |
|---|---|---|
| V1 | `modulos.status = 1` e `configurations.newsletters = 0` | o formulário aparece no layout ativo |
| V2 | `modulos.status = 0` | o formulário **não** aparece |
| V3 | `configurations.newsletters = 1` | o formulário **não** aparece (o rodapé do modelo5 muda de classe CSS) |
| V4 | Layout `default` | o formulário aparece (na v6 **não** aparece — divergência a corrigir) |

## 7. Como rodar

```bash
# v7 (Laravel moderno)
php artisan test --filter=Newsletter
php artisan test tests/Feature/Newsletter/PortalSubscriptionTest.php
php artisan test tests/Feature/Newsletter/CmsManagementTest.php

# cobertura
php artisan test --coverage --min=80
```

No legado (v6, sem testes do módulo), a verificação é manual:

```bash
# listar as assinaturas
docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 \
  -c "select id, name, email, ip_address, sent_at, active, deleted_at from newsletters order by id desc limit 20;"

# conferir o que o CSV exportado entrega (inclui soft delete)
docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 \
  -c "select count(*) as total, count(*) filter (where deleted_at is null) as visiveis from newsletters;"
```

## 8. Matriz de cobertura mínima

| Comportamento | Teste |
|---|---|
| Assinatura pelo portal | P1, P5, P8 |
| E-mail único | P2, C6, C7 |
| Validação de `name` | C4, C5 |
| Soft delete | C8, C9, C10 |
| Exportação | C10, C11, C12 |
| Exibição no portal | V1, V2, V3, V4 |
| Permissões | C13, C14 |
