# Terceirizados — Estratégia de Testes

> Módulo **existente apenas na `v5`**. A v5 **não possui** suíte de testes para este
> módulo (nem testes automatizados do portal em geral). O conteúdo abaixo é a
> especificação de testes a criar na v7 (PHP 8.x / Laravel 11-12).

---

## 1. Inventário do que existe hoje na v5

| Item | Existe? | Observação |
|---|---|---|
| `v5/tests/Unit/` | Diretório existe | Sem caso específico de Terceirizados. |
| `v5/tests/Feature/` | Diretório existe | Sem caso específico de Terceirizados. |
| Testes do controller CMS | Não | — |
| Testes do controller do portal | Não | — |
| Testes do `OutsourcedRequest` | Não | — |
| Testes do `FilterOutsourceds` | Não | — |
| Factories | Não | `v5/database/factories/` não possui factory de `Outsourced` (a tabela não tem factory). |
| Seeds de teste | Não | Nenhum seed com dados de terceirizados (a tabela está vazia no banco v5). |

> Consequência: toda a validação do módulo na v5 foi **manual**. Os casos abaixo
> formalizam o que deveria ser automatizado.

## 2. Estratégia

| Camada | Ferramenta | Escopo |
|---|---|---|
| **Feature (portal)** | `Laravel\Testing\FeatureTest` + `RefreshDatabase` | Rotas públicas `/terceirizados`, filtros, paginação, ficha, exportações, ocultação por `is_show`. |
| **Feature (CMS)** | `FeatureTest` + `actingAs` com usuário Sentinel | CRUD, upload, remoção de anexo, permissões, bulk delete, auditoria. |
| **Unit (model)** | `PHPUnit\Framework\TestCase` + `RefreshDatabase` | Casts, soft delete, accessor de anexos, descrição do log de atividade. |
| **Unit (filtro)** | `TestCase` + `RefreshDatabase` | `FilterOutsourceds` com e sem cada parâmetro. |
| **Unit (exportação)** | `TestCase` + `RefreshDatabase` | Conteúdo gerado pelo `OutsourcedsExport` (headings e mapeamento). |

Regras transversais:

- Banco de teste: **PostgreSQL** (o `ilike` e o `whereNull` do legado não funcionam em
  SQLite/MySQL). Usar `.env.testing` apontando para o container do projeto.
- `Storage::fake('public')` em todos os testes que gravam arquivo, para não sujar o disco.
- `Date::setTestNow()` nos testes de `year` (a regra depende de `date('Y')`).
- Fuso horário fixo (`America/Sao_Paulo`) para os timestamps.

## 3. Fixtures necessárias

### Factory

```php
// (ilustrativo) database/factories/OutsourcedFactory.php
namespace Database\Factories;

use App\Models\Outsourced;
use Illuminate\Database\Eloquent\Factories\Factory;

class OutsourcedFactory extends Factory
{
    protected $model = Outsourced::class;

    public function definition(): array
    {
        return [
            'year'          => 2026,
            'month'         => 3,
            'employee_name' => fake()->name(),
            'company'       => fake()->company(),
            'role'          => fake()->randomElement(['Auxiliar', 'Analista', 'Tecnico']),
            'is_show'       => true,
        ];
    }

    public function hidden(): static
    {
        return $this->state(fn () => ['is_show' => false]);
    }
}
```

### Cenário base de dados

| Factory | Uso |
|---|---|
| `Outsourced::factory()->count(20)->create()` | Alimenta paginação (15 por página) e exportações. |
| `Outsourced::factory()->hidden()->create()` | Verifica que o portal não exibe registros ocultos. |
| `Outsourced::factory()->create(['year' => 2025, 'month' => 12])` | Filtros por ano/mês. |
| `Outsourced::factory()->create(['employee_name' => 'Maria de Souza', 'company' => 'Contratada Exemplo LTDA'])` | Busca textual. |

### Seed de ambiente (opcional)

Para o ambiente de demonstração, um seeder com 12 registros (um por mês de 2026) e um
anexo PDF fictício em `storage/app/public/terceirizados/`. Não é necessário para os
testes automatizados.

## 4. Casos de teste — Portal

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Lista pública | 20 registros `is_show = true` | GET `/terceirizados` | Status 200; view `portal.layouts.<layout>.terceirizados.index`; 15 itens na tabela; paginação presente. |
| P2 | Registro oculto | 1 registro `is_show = false` + 1 visível | GET `/terceirizados` | O oculto **não** aparece na tabela. |
| P3 | Filtro por ano | registros em 2025 e 2026 | GET `/terceirizados?ano=2026` | Só os de 2026 na tabela; `<option selected>` no filtro. |
| P4 | Filtro por mês | registros em janeiro e março | GET `/terceirizados?mes=3` | Só os de março. |
| P5 | Filtro por função | `role = 'Auxiliar'` e `role = 'Analista'` | GET `/terceirizados?funcao=Anal` | Só os de "Analista" (match parcial, `ilike`). |
| P6 | Busca livre (nome) | `employee_name = 'Maria de Souza'` | GET `/terceirizados?busca=Maria` | Registro encontrado. |
| P7 | Busca livre (empresa) | `company = 'Contratada Exemplo'` | GET `/terceirizados?busca=Exemplo` | Registro encontrado. |
| P8 | Busca sem resultado | — | GET `/terceirizados?busca=zzzz` | 200 e a mensagem "Nenhuma informação encontrada!". |
| P9 | Busca com `%` do usuário | — | GET `/terceirizados?busca=%25` | 200 sem erro (não pode gerar SQL inválido); avaliar se o resultado deve ser vazio (curinga escapado). |
| P10 | Ficha | 1 registro | GET `/terceirizados/{id}` | 200; exibe Ano, Mês por extenso, Nome, Empresa, Função. |
| P11 | Ficha de registro oculto | registro `is_show = false` | GET `/terceirizados/{id}` | **404**. |
| P12 | Ficha inexistente | — | GET `/terceirizados/999999` | **404**. |
| P13 | Exportação XLSX | 3 registros | GET `/terceirizados?type=xls` | Status 200; header `Content-Disposition` com `terceirizados.xlsx`. |
| P14 | Exportação CSV | 3 registros | GET `/terceirizados?type=csv` | 200; arquivo `terceirizados.csv`. |
| P15 | Exportação PDF | 3 registros | GET `/terceirizados?type=pdf` | 200; `terceirizados.pdf`. |
| P16 | Exportação TXT | 3 registros | GET `/terceirizados?type=txt` | 200; `terceirizados.txt`; conteúdo contém o cabeçalho "Ano  \| Mês". |
| P17 | Exportação respeita filtros | 5 registros, filtro `ano=2026` | GET `/terceirizados?ano=2026&type=csv` | O arquivo contém **apenas** os registros de 2026. |
| P18 | Exportação não pagina | 40 registros | GET `/terceirizados?type=csv` | O arquivo contém os 40 registros (não 15). |
| P19 | Manutenção do site | `configurations.pagina_em_manutencao = 1` | GET `/terceirizados` | Redirecionamento/bloqueio pelo middleware `check.site.manutention`. |

## 5. Casos de teste — CMS

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | Listagem no CMS | usuário com `terceirizados.index` | GET `/cms/terceirizados` | 200; view `prefecture.outsourceds.index`; log de atividade "Listou Terceirizados" criado. |
| C2 | Sem permissão | usuário sem `terceirizados.index` | GET `/cms/terceirizados` | Redirect (middleware `check.route.permission`). |
| C3 | Cadastro sem anexo | — | POST `/cms/terceirizados` com `year, month, employee_name, company, role` | 302 para `terceirizados.index`; registro criado com `is_show = true` e `file_path = null`. |
| C4 | Cadastro com anexo PDF | — | POST com `arquivo[]` (PDF fake) + `arquivo_desc[]` | Arquivo gravado em `terceirizados/`; `file_path` contém JSON com `path` e `description`. |
| C5 | Cadastro com anexo não-PDF | — | POST com `arquivo[]` (`.txt`) | 422; nenhum registro criado. |
| C6 | Validação de ano | — | POST com `year = 1990` | 422 com erro no campo `year`. |
| C7 | Validação de ano futuro | — | POST com `year = date('Y') + 5` | 422. |
| C8 | Validação de mês | — | POST com `month = 13` | 422. |
| C9 | Campos obrigatórios | — | POST sem `company` e `role` | 422; mensagens "O campo Empresa é obrigatório" / "O campo Função é obrigatório". |
| C10 | Limite de tamanho do texto | — | POST com `employee_name` de 300 chars | 422. |
| C11 | Edição soma anexos | registro com 1 anexo | PUT com 1 novo anexo | `file_path` passa a ter **2** itens; o arquivo antigo continua disponível. |
| C12 | Edição sem novo anexo | registro com 1 anexo | PUT sem `arquivo` | `file_path` **inalterado** (o `file_path` não é sobrescrito com `null`). |
| C13 | Remoção de anexo | registro com 2 anexos | DELETE `/cms/terceirizados/{id}/arquivo/0` | JSON `{success}`; `file_path` com 1 item; o índice foi reindexado (`array_values`). |
| C14 | Remoção de anexo inexistente | registro com 1 anexo | DELETE `.../arquivo/5` | JSON `{erro}` com status 404. |
| C15 | Exclusão individual | registro | DELETE `/cms/terceirizados/{id}` | JSON `{success}`; `deleted_at` preenchido; o portal deixa de listá-lo. |
| C16 | **Exclusão em lote** | 3 registros | POST `/cms/terceirizados/destroyAll` com `toDelete[]` | Os 3 são deletados (**bug A1**: na v5 o controller lê `ids` e nada é apagado — o teste novo deve usar o nome de campo correto e o legado deve ser corrigido). |
| C17 | Auditoria de criação | — | POST de cadastro | Registro em `activity_log` com `log_name = outsourced` e descrição contendo o nome do empregado. |
| C18 | `show` redireciona ao portal | registro | GET `/cms/terceirizados/{id}` | 302 para `portal.terceirizados.show`. |
| C19 | Editar inexistente | — | GET `/cms/terceirizados/999999/edit` | 302 para `terceirizados.index` com flash de erro. |
| C20 | DataTable JSON | 3 registros | GET `/cms/datatable/terceirizados` | JSON com `data`, `recordsTotal` e `mes_extenso` preenchido. |

## 6. Casos de teste — Unitários

| # | Alvo | Cenário | Assert |
|---|---|---|---|
| U1 | `Outsourced` |casts | `year`/`month` voltam como `int`; `is_show` como `bool`. |
| U2 | `Outsourced` | soft delete | `deleted_at` preenchido; query padrão não retorna o registro. |
| U3 | `Outsourced` | descrição do log | `getDescriptionForEvent('created')` contém o nome e o evento. |
| U4 | `FilterOutsourceds` | sem parâmetros | Só retorna `is_show = true`. |
| U5 | `FilterOutsourceds` | `ano` + `mes` combinados | Interseção correta. |
| U6 | `FilterOutsourceds` | `busca` casa por nome **ou** empresa | Union dentro do closure (`orWhere` agrupado). |
| U7 | `FilterOutsourceds` | `funcao` com acento | Compara com `ilike` (case-insensitive, mas **não** accent-insensitive) — documentar o comportamento. |
| U8 | `FilterOutsourceds` | `busca = '0'` | Falsy: o filtro **não** é aplicado (bug latente). |
| U9 | `OutsourcedsExport` | headings | `['Ano', 'Mês', 'Nome do Empregado', 'Empresa', 'Função']`. |
| U10 | `OutsourcedsExport` | mapeamento | Linha com o mês por extenso e os demais campos. |

## 7. Fixtures de view/seed

| Item | Finalidade |
|---|---|
| Configuração `configurations.key = layout` = `modelo1` | Garantir que a view do portal exista no teste (o `getTemplateActived()` aborta com 404 se `portal.layouts.<layout>.home.index` não existir). |
| `resources` com slug `terceirizados` e as ações | Necessária para `has_access` e para o middleware de rota. |
| `menu_orders` com slug `terceirizados` | Necessária para o item de menu e para `menuClicks()` (sem a linha, o contador simplesmente não é incrementado — sem erro). |
| Usuário Sentinel + permissões | Para os testes do CMS: `Sentinel::getUser()->addPermission(...)` ou o mechanism equivalente de `hasAnyAccess`. |

## 8. Como rodar

Ambiente Laravel moderno (v7):

```bash
# banco de teste
php artisan migrate:fresh --env=testing

# suíte completa
php artisan test

# apenas o módulo
php artisan test --filter=Outsourced

# pasta específica
php artisan test tests/Feature/Portal/OutsourcedTest.php
php artisan test tests/Feature/Admin/OutsourcedTest.php
php artisan test tests/Unit/Models/OutsourcedTest.php

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

Variáveis de ambiente para o container de teste:

```bash
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=prefeituras_v7_test
DB_USERNAME=postgres
DB_PASSWORD=secret
FILESYSTEM_DISK=public
```

## 9. Checklist manual (equivalente ao que era feito na v5)

1. Entrar no CMS com usuário autorizado e abrir `/cms/terceirizados`.
2. Cadastrar um terceirizado **sem** anexo → confirmar redirecionamento e flash verde.
3. Cadastrar **com** 2 anexos PDF e descrições → conferir os dois na lista de
   "Arquivos enviados" e o conteúdo de `file_path` no banco.
4. Tentar anexar um `.docx` → a validação deve recusar (mensagem "O campo Anexo / Arquivo
   deve ser um arquivo do tipo: pdf").
5. Editar o registro **anexando mais um arquivo** → os três devem constar da lista.
6. Remover o primeiro anexo pela lixeira → os demais permanecem, renumerados.
7. Excluir o registro → o portal deixa de exibi-lo.
8. No portal, filtrar por ano, mês, função e texto → conferindo a URL com os parâmetros.
9. Exportar em xls, pdf, csv e txt e abrir os 4 arquivos.
10. Auditar: verificar a entrada em `/cms/log` com "Listou Terceirizados" e com as
    descrições das alterações.
11. Verificar o incremento de `menu_orders.clicks` após abrir a listagem do CMS.
