# Vereadores e Mandatos — Estratégia de Testes

> Módulo `vereadores` (id 43). A v6 **não possui nenhum teste** para este módulo
> (não há suíte automatizada no repositório legado); o plano abaixo é o alvo da v7.

---

## 1. Estratégia

| Camada | Ferramenta | Cobertura |
|---|---|---|
| Feature (portal) | PHPUnit + `RefreshDatabase` + `assertViewHas` | Regras de visibilidade, ordenação, agrupamento por categoria, caminho da foto. |
| Feature (CMS) | PHPUnit + `actingAs` | Autorização, CRUD, recusa de exclusão, upload, atribuição de mandato, ordenação. |
| Unit | PHPUnit | Regras puras: slug único, abreviação de nome, cálculo de `order`. |
| API | PHPUnit (`JsonResource`) | Formato de saída dos Resources. |
| E2E | Laravel Dusk (opcional) | Arrastar-e-soltar da ordenação e preview da foto no formulário. |

Regras de teste que reproduzem as regras de negócio do legado:

- Um vereador com `status = false` **não** aparece na listagem pública.
- Um vereador com mandatos **não** pode ser excluído (a Policy/regra de negócio deve
  barrar, e não apenas a view).
- O `role` é obrigatório na atribuição de mandato.
- A ordem persistida no pivot é a ordem exibida no portal.

## 2. Fixtures / Factories

```php
// tests/Models/Concerns/HasVereadores.php (trait de conveniência)

CityCouncilor::factory()->create(['name' => 'João da Silva', 'party' => 'PSD', 'status' => true]);
MandateCategory::factory()->create(['name' => 'Legislaturas', 'slug' => 'legislaturas']);
Mandate::factory()->create([
    'name' => 'Legislatura 2021-2024',
    'slug' => 'legislatura-2021-2024',
    'start_year' => 2021,
    'end_year' => 2024,
    'mandate_categories_id' => $category->id,
    'status' => true,
]);
```

Factories necessárias:

| Factory | Estado |
|---|---|
| `CityCouncilorFactory` | `active()` / `inactive()`, `withPhoto()` |
| `MandateCategoryFactory` | default "Legislaturas" |
| `MandateFactory` | `withCategory()`, `active()` |
| `CouncilmenMandatesFactory` | `withRole('Presidente')`, `withOrder(1)` |
| `ArchiveFactory` | `image()` (arquivo de imagem real em `storage/framework/testing`) |
| `UserFactory` / `RoleFactory` | papéis do CMS (Sentinel → Spatie Permission na v7) |

Seed de ambiente de teste: recriar as três categorias de
`v6/database/seeds/MandateCategoryTableSeeder.php` (`mesas-diretoras`,
`legislaturas`, `comissoes-permanentes`) e um layout com o carrossel
(`vereadores` / `vereadores-carousel`) equivalente ao seed
`v6/database/seeds/AddVereadoresCarouselToLayout.php`.

## 3. Casos de teste

### 3.1 Portal

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | 1 vereador ativo, 1 inativo | `GET /vereadores` | A view contém o nome do ativo e **não** contém o do inativo. |
| P2 | Vereador ativo com foto | `GET /vereadores` | A resposta contém `/storage/content/vereadores/{id}/{nome}`. |
| P3 | Vereador ativo sem foto | `GET /vereadores` | A resposta contém `/assets/images/perfil.jpg`. |
| P4 | Vereador inativo | `GET /vereadores/{id}` | 200 (comportamento atual da v6 — **a decidir** se deve ser 404). |
| P5 | Vereador com 2 mandatos | `GET /vereadores/{id}` | A view contém o `role` de cada mandato (`pivot->role`). |
| P6 | 2 categorias, 3 mandatos | `GET /mandatos` | A view lista as 2 categorias com link `route('portal.mandato', $slug)`. |
| P7 | Categoria "Legislaturas" com 2 mandatos | `GET /mandatos/legislaturas` | A view contém os 2 mandatos. |
| P8 | Slug de categoria inexistente | `GET /mandatos/inexistente` | Response 200 e nenhum mandato listado (comportamento atual). |
| P9 | Mandato com 3 vereadores, `order` 3,1,2 | `GET /mandatos/{slug}` | A ordem exibida é 1, 2, 3 (a query ordena por `order`). |
| P10 | 2 vereadores ativos | `GET /` (home `modelo5`) | A home renderiza o carrossel com os 2 nomes. |
| P11 | Nenhum vereador ativo | `GET /` (home `modelo5`) | O bloco `#councilmen-carousel` não é renderizado. |

### 3.2 CMS — Vereadores

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | Usuário sem `vereadores.create` | `GET /cms/vereadores/create` | 403 (ou redirect de aviso, conforme o middleware). |
| C2 | Usuário com `vereadores.create` | `GET /cms/vereadores/create` | 200. |
| C3 | Usuário com `vereadores.edit` | `GET /cms/vereadores/{id}/edit` | 200. |
| C4 | — | `POST /cms/vereadores` sem `name` | 302 com erro de validação "O campo nome é obrigatório.". |
| C5 | — | `POST /cms/vereadores` sem `party` | 302 com erro "O campo partido é obrigatório.". |
| C6 | — | `POST /cms/vereadores` com `archives_id` não-imagem | 302 com erro "Selecione uma imagem válida.". |
| C7 | — | `POST /cms/vereadores` válido | Registro criado, `slug = Str::slug(name)`, arquivo salvo em `content/vereadores/{id}/`. |
| C8 | Vereador existente com o mesmo nome | `POST /cms/vereadores` | `slug` termina com sufixo `_xxxxx` e não colide. |
| C9 | — | `POST /cms/vereadores` sem o campo `status` | `status = false` no banco (regra R11). |
| C10 | Vereador **sem** mandatos | `DELETE /cms/vereadores/{id}` | 302; registro com `deleted_at` preenchido. |
| C11 | Vereador **com** mandato | `DELETE /cms/vereadores/{id}` | 302; **não** deletado; mensagem "Esse vereador tem mandatos ativos, não pode ser excluído.". |
| C12 | Vereador com nome alterado | `PUT /cms/vereadores/{id}` | `slug` **inalterado** (comportamento atual da v6) ou alterado — decisão registrada. |
| C13 | Vereador com foto | `POST /cms/vereadores/{id}/remover-foto` (ajax) | Arquivo removido do storage, `archives_id = null`, `archives` deletado. |

### 3.3 CMS — Mandatos e categorias

| # | Arrange | Act | Assert |
|---|---|---|---|
| M1 | — | `POST /cms/mandatos` sem `mandate_categories_id` | 302 com erro "O campo categoria é obrigatório.". |
| M2 | — | `POST /cms/mandatos` sem `start_year` | 302 com erro "O campo ano de início é obrigatório.". |
| M3 | Mandato existente, mesmo `name` | `PUT /cms/mandatos/{id}` | **Comportamento atual da v6**: o slug muda (bug). Teste deve documentar a decisão da v7. |
| M4 | Mandato com 3 vereadores | `PATCH /cms/mandatos/{id}/councilmen/order` com `order=[3,1,2]` | `order` dos 3 registros = 1, 2, 3 na ordem enviada. |
| M5 | Mandato inexistente | `PATCH /cms/mandatos/999/councilmen/order` | 404 / JSON de erro. |
| M6 | Categoria de mandato com 1 mandato | `DELETE /cms/categorias-mandatos/{id}` | Bloqueado o soft delete — definir a política (a FK `restrictOnDelete` sugere bloquear). |
| M7 | Mandato de categoria inexistente | `POST /cms/mandatos` com `mandate_categories_id` inválido | Rejeitado pela FK (hoje vira flash genérico por `PDOException`). |

### 3.4 CMS — Atribuição de mandato

| # | Arrange | Act | Assert |
|---|---|---|---|
| A1 | Vereador e mandato existentes | `POST /cms/vereadores/atribuir-mandato` com `mandates_id` e `role` | Linha criada em `councilmen_mandates` com `role` preenchido. |
| A2 | — | `POST` sem `role` | 302 com erro "O campo cargo é obrigatório.". |
| A3 | — | `POST` sem `mandates_id` | 302 com erro "O campo mandato é obrigatório.". |
| A4 | Associação existente | `POST /cms/vereadores/excluir-mandato` com `id` do pivot | Linha removida; se optou-se por `SoftDeletes`, `deleted_at` preenchido. |
| A5 | Mandato atribuído | `GET /cms/vereadores/{id}` | A tabela lista o mandato com o botão de exclusão. |

### 3.5 Autorização

| # | Arrange | Act | Assert |
|---|---|---|---|
| Z1 | Usuário `super-admin` | Todas as rotas do CMS | 200/302 conforme esperado. |
| Z2 | Usuário sem nenhuma permissão | `GET /cms/vereadores` | Redirecionamento com aviso (comportamento do `CheckRoutePermission`). |
| Z3 | Usuário com `vereadores.edit` mas não `vereadores.delete` | `GET /cms/vereadores` | O botão de exclusão **não** é renderizado (corrige o bug `agenda.delete`). |
| Z4 | Usuário com `mandatos.edit` | `GET /cms/mandatos` | Coluna de ações e botões visíveis. |

## 4. Código de exemplo (feature test do portal)

```php
// tests/Feature/Portal/CityCouncilorTest.php
public function test_listagem_publica_mostra_somente_vereadores_ativos(): void
{
    $ativo = CityCouncilor::factory()->create(['name' => 'Vereador Ativo', 'status' => true]);
    $inativo = CityCouncilor::factory()->create(['name' => 'Vereador Inativo', 'status' => false]);

    $this->get(route('vereadores'))
        ->assertOk()
        ->assertViewHas('contents', fn ($contents) => $contents->contains('id', $ativo->id))
        ->assertDontSee('Vereador Inativo');
}
```

```php
// tests/Feature/Cms/CityCouncilorTest.php
public function test_nao_exclui_vereador_com_mandatos(): void
{
    $councilor = CityCouncilor::factory()->create();
    $mandate = Mandate::factory()->create();
    $councilor->mandates()->attach($mandate->id, ['role' => 'Presidente']);

    $this->actingAs($this->superAdmin())
        ->delete(route('vereadores.destroy', $councilor))
        ->assertSessionHas('error');

    $this->assertNotNull(CityCouncilor::withTrashed()->find($councilor->id));
}
```

## 5. Comandos exatos

```bash
# Instalar dependências de teste (v7)
composer install

# Migrar o banco de testes (SQLite em memória é suficiente)
php artisan migrate:fresh --env=testing

# Rodar toda a suíte
php artisan test

# Rodar apenas os testes do módulo
php artisan test --filter=CityCouncilor
php artisan test --filter=Mandate
php artisan test tests/Feature/Portal tests/Feature/Cms

# Com cobertura (requer Xdebug ou PCOV)
php artisan test --coverage --min=80

# Testes em paralelo (requer ParaTest)
php artisan test --parallel

# Análise estática (Laravel Pint + PHPStan)
vendor/bin/pint --test
vendor/bin/phpstan analyse app tests --level=6

# Link do storage de arquivos (necessário para os testes de upload)
php artisan storage:link
```

No legado (v6), os comandos disponíveis são apenas os do PHPUnit puro, sem suíte
para este módulo:

```bash
cd v6
./vendor/bin/phpunit
```

## 6. Checklist de migração de testes

- [ ] Criar `CityCouncilorFactory`, `MandateFactory`, `MandateCategoryFactory`,
      `CouncilmenMandatesFactory`, `ArchiveFactory`.
- [ ] Criar `CityCouncilorPolicy` e `MandatePolicy` + testes unitários.
- [ ] Criar `CityCouncilorResource` e `MandateResource` + teste de estrutura JSON.
- [ ] Mover `trata_nome()` para um helper `Str::headline`-like e testá-lo.
- [ ] Testar o `SlugService` (colisão e unicidade).
- [ ] Testar o job de geração de thumbnail.
- [ ] Testar o `scopeActive` e a regra de listagem pública.
- [ ] Testar a ordenação ponta a ponta (controller → pivot → view).
