# Arquivo (Drive) — Testes

> **Escopo:** v5 (para documentar o que **não** é testado) e plano de testes
> para a v7.

---

## 1. Estado atual na v5

| Item | Situação |
|---|---|
| Suíte de testes | `v5/tests/Feature/` (apenas `ExampleTest.php`) e `v5/tests/Unit/` (`CnpjAlphanumericValidationTest.php`, `ExampleTest.php`). |
| Testes do módulo de arquivo | **NENHUM**. Verificado por `grep -rli "archive" v5/tests/` → 0 resultados. |
| Testes do `ArchiveController` | Nenhum. |
| Testes do `ArchiveCategoryController` | Nenhum. |
| Testes de model | Nenhum. |
| Testes de upload/ordering | Nenhum. |
| Cobertura de segurança (rotas sem auth) | Nenhuma — o bug documentado em `08-GAPS-E-DIVIDAS.md` B01 passou despercebido. |

Consequência: **todas as regras descritas neste dossiê foram descobertas
por leitura de código e execução manual**, não por testes.

## 2. Estratégia para a v7

| Camada | Ferramenta | Foco |
|---|---|---|
| **Unit** | PHPUnit / Pest | Model: casts, scopes, accessor `public_url`, geração de slug, cálculo de `position`. |
| **Feature (API)** | Laravel `TestCase` + `Storage::fake('public')` | Upload, listagem, reordenação, exclusão, categorias, **autorização**. |
| **Feature (portal)** | Laravel `TestCase` | Renderização do bloco de arquivos e da galeria; arquivo ausente não quebra a página. |
| **Integração** | `DatabaseTransactions` | Reordenação em lote, `ON DELETE SET NULL` de categoria, FKs. |

### 2.1 Como rodar

```bash
cd /path/da/v7

# suíte completa
php artisan test

# apenas o módulo de arquivo
php artisan test --filter=Archive

# pasta específica
./vendor/bin/phpunit tests/Feature/Archive

# unitário
./vendor/bin/phpunit tests/Unit/ArchiveTest.php
```

> A v5 usa `phpunit ~6.0` (`v5/composer.json:50`) e a v6 também
> (`v6/composer.json:50`). Na v7 (Laravel 11/12) o alvo é
> `phpunit/phpunit ^11` com `pestphp/pest` opcional. O `phpunit.xml` da
> v6 usa `<testsuite name="Feature">` + `<testsuite name="Unit">` — a v7
> deve manter a mesma convenção de pastas.

## 3. Fixtures e factories necessárias

```php
// database/factories/ArchiveFactory.php
class ArchiveFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title'       => $this->faker->sentence(3),
            'description' => $this->faker->sentence(6),
            'name'        => $this->faker->word().'.pdf',
            'path'        => 'content/test/'.$this->faker->uuid().'.pdf',
            'extension'   => 'pdf',
            'mime_type'   => 'application/pdf',
            'size_bytes'  => 10240,
            'hash'        => hash('sha256', $this->faker->uuid()),
            'position'    => 0,
            'view_pdf'    => false,
            'is_featured' => false,
        ];
    }

    public function image(): static
    {
        return $this->state(fn () => [
            'name'       => $this->faker->uuid().'.jpg',
            'path'       => 'content/test/'.$this->faker->uuid().'.jpg',
            'extension'  => 'jpg',
            'mime_type'  => 'image/jpeg',
        ]);
    }

    public function featured(): static
    {
        return $this->state(fn () => ['is_featured' => true]);
    }
}
```

```php
// database/factories/ArchiveCategoryFactory.php
class ArchiveCategoryFactory extends Factory
{
    public function definition(): array
    {
        $name = $this->faker->unique()->words(2, true);
        return ['name' => $name, 'slug' => Str::slug($name), 'position' => 0];
    }
}
```

Fixtures de seed para ambiente de teste:

| Seed | Conteúdo |
|---|---|
| `ArchiveCategorySeeder` | As 4 categorias da v5 (`Documentos Oficiais`, `Relatórios`, `Fotos`, `Vídeos`). |
| `TypeSizeSeeder` | `Grande/600px`, `Médio/350px`, `Pequeno/200px`, `Personalizado/null`. |
| Usuário de teste | Seguindo o padrão da v6 (`v6/tests/DashboardTestCase.php:93-118`): `Sentinel::registerAndActivate()` + papel `super-admin` + `Sentinel::authenticate()`. |

## 4. Casos de teste — API

### 4.1 Upload

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| U01 | Upload simples no Drive | usuário autenticado; `Storage::fake('public')` | `POST /api/v1/archives` com `file` fake | 201; arquivo em `archives`; `path` relativo (sem `storage/`); `Storage::disk('public')->assertExists($path)` |
| U02 | Upload com nome original preservado | idem | idem | `name` = nome original; `extension` = extensão real |
| U03 | Extensão não permitida | idem | `file` com `.exe` | 422; **nenhuma** linha criada; **nenhum** arquivo no disco |
| U04 | Arquivo acima do limite | idem | `file` de 129 MB | 422 |
| U05 | MIME incompatível com a extensão | idem | `.jpg` com conteúdo de texto | 422 |
| U06 | `category_id` inexistente | idem | `category_id = 999999` | 422 (FK) — **a v5 não valida** (`05-REGRAS-E-PORTABILIDADE.md` A11) |
| U07 | Upload com `attachable_type`/`attachable_id` | idem | `attachable_type=content`, `attachable_id=1` | linha com os dois campos preenchidos |
| U08 | `attachable_id` sem `attachable_type` | idem | só `attachable_id` | 422 |
| U09 | `view_pdf` verdadeiro | idem | `view_pdf=1` | coluna gravada; aparece no embed do portal |
| U10 | Upload sem permissão `drive.create` | usuário sem a permissão | `POST` | **403** — a v5 responde **200** (B01) |
| U11 | Upload sem autenticação | — | `POST` | **401/302** — a v5 responde 200 |
| U12 | Nome de arquivo malicioso | idem | `file` com nome `../../evil.php` | o caminho final fica **dentro** do diretório esperado; nenhum `..` no `path` |
| U13 | Extensão dupla | idem | `file` com nome `a.php.jpg` | `extension` = `jpg`; o arquivo não é executável |

### 4.2 Listagem

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| L01 | Lista por escopo | 3 arquivos, 2 do mesmo conteúdo, 1 de outro | `GET /api/v1/archives?attachable_type=content&attachable_id=1` | 2 itens |
| L02 | Filtro por categoria | 3 arquivos, 2 da mesma categoria | `GET …?category_id=X` | 2 itens |
| L03 | Busca por nome | arquivos com nomes distintos | `GET …?search=relatorio` | apenas o correspondente |
| L04 | Paginação | 120 arquivos | `GET …?per_page=50` | 50 itens + `meta.total = 120` — **a v5 não pagina** (A20) |
| L05 | Ordenação | arquivos com `position` 3,1,2 | `GET …` | ordem 1,2,3 (`position ASC`) |
| L06 | Soft delete oculta da listagem | arquivo deletado | `GET …` | não aparece |
| L07 | Escopo inválido | — | `GET …?attachable_type=invalido` | 422 |

### 4.3 Reordenação

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| R01 | Reordenar em lote | 3 arquivos do mesmo conteúdo | `PUT /api/v1/archives/order` com `items` | `position` = 1,2,3 conforme a ordem enviada |
| R02 | **Escopo obrigatório** | arquivo de outro conteúdo na lista | idem | 422 ou o arquivo **fora do escopo** é ignorado — a v5 trocaria a ordem entre contextos (A05) |
| R03 | Transação | lista com id inexistente no meio | idem | **nenhuma** `position` alterada (rollback) |
| R04 | `items.*.id` duplicado | idem | idem | 422 (`distinct`) |
| R05 | Reordenar sem permissão | — | idem | 403 |
| R06 | `position` sem buraco | 2 arquivos | reordenar | `position` contíguo (1,2) — sem "buracos" |

### 4.4 Exclusão

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| D01 | Soft delete | arquivo + binário | `DELETE /api/v1/archives/{id}` | `deleted_at` preenchido; listagem não retorna; binário ainda existe (comportamento da v5) |
| D02 | Limpeza do binário | idem | executar o job `DeleteArchiveFile` | binário removido |
| D03 | Excluir sem permissão | — | `DELETE` | 403 |
| D04 | Arquivo já deletado | `deleted_at` preenchido | `DELETE` | 404 (ou 200 idempotente — decidir e documentar) |

### 4.5 Categorias

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C01 | Listar | 3 categorias, 1 deletada | `GET /api/v1/archive-categories` | 2 categorias (a deletada não aparece) |
| C02 | Criar | — | `POST` com `name` | 201; `slug` gerado em **minúsculas** com hífen (`Str::slug`) — a v5 gerava `TESTEPROBE` (A09) |
| C03 | Slug duplicado | categoria "Relatórios" | `POST` com `name=Relatórios` | **422** (unique) — a v5 devolve **500** (A10) |
| C04 | Nome vazio | — | `POST` com `name=""` | 422 — a v5 dá erro 500 (índice indefinido) |
| C05 | Renomear | — | `PATCH` | nome e slug atualizados |
| C06 | Excluir categoria com arquivos | 3 arquivos na categoria | `DELETE` | categoria deletada; os 3 arquivos com `category_id = NULL` (`ON DELETE SET NULL`) — **comportamento novo**, a v5 não tem exclusão |
| C07 | Slug com acento | — | `POST` com `name=Ações` | `slug = acoes` |

## 5. Casos de teste — portal

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P01 | Bloco de arquivos | 2 arquivos | `GET /página` | ambos listados com `description or name` |
| P02 | Arquivo ausente no disco | `path` apontando para arquivo inexistente | `GET /página` | **a página renderiza** sem o item e **sem erro** (equivalência E2) |
| P03 | Descrição substitui o nome | arquivo com `description` | `GET /página` | mostra a descrição (equivalência E3) |
| P04 | PDF embutido | arquivo com `view_pdf=true` | `GET /página` (desktop) | `<iframe>` com o `path` |
| P05 | Mobile sem embed | mesmo | `GET /página` com User-Agent de iPhone | **sem** `<iframe>`; só a lista |
| P06 | Galeria só com imagens | arquivos `.jpg` e `.pdf` | `GET /página` | galeria só com o `.jpg` |
| P07 | Destaque fora da galeria | `is_featured=true` | `GET /página` | não aparece na galeria de fotos |
| P08 | Arquivo sem `extension` | — | `GET /página` | não aparece na galeria |

## 6. Casos de teste — segurança (prioridade máxima)

> Estes testes existem porque a v5 tem o bug B01. Devem ser escritos
> **antes** de qualquer refactor da v7.

| # | Cenário | Act | Assert |
|---|---|---|---|
| S01 | Listar categorias sem sessão | `GET /cms/drive/categorias` | **302 → login** (v5: 200 com o JSON) |
| S02 | Criar categoria sem sessão | `POST /cms/drive/cadastrar/categoria` | **302 → login** (v5: 200 e **cria**) |
| S03 | Listar galeria sem sessão | `GET /cms/galeria` | 302/401 |
| S04 | Upload sem sessão | `POST /cms/galeria` | 302/401 |
| S05 | Excluir arquivo sem sessão | `POST /cms/galeria/delete/{id}` | 302/401 e `deleted_at` **inalterado** |
| S06 | Reordenar sem sessão | `POST /cms/drive/atualizar-ordem` | 302/401 e `order` **inalterado** |
| S07 | Reordenar sem permissão `drive.update` | usuário logado sem a permissão | 403 |
| S08 | Upload sem permissão `drive.create` | idem | 403 |
| S09 | Token CSRF inválido | `POST` com token errado | 419 |
| S10 | Sessão expirada no meio do upload | token antigo | 419 (a v5 redireciona para `/logout`) |
| S11 | `attachable_id` de outro tenant | usuário sem acesso ao conteúdo | 403 |

## 7. Casos de teste — model / unit

| # | Cenário | Assert |
|---|---|---|
| M01 | `scopeGalleries` | exclui `is_featured = true` e `extension = null` |
| M02 | `scopeImages` | 11 extensões esperadas |
| M03 | Accessor `public_url` | `Storage::url($path)`; nunca começa com `storage/` duplicado |
| M04 | Slug automático | `Str::slug` minúsculo com hífen |
| M05 | Casts | `view_pdf`/`is_featured` boolean; `position`/`width`/`height`/`size_bytes` integer |
| M06 | `nextPosition` | retorna `max(position)+1` dentro do escopo |
| M07 | Relations | `attachable` resolve para o modelo correto por `attachable_type` |
| M08 | `works` com `withPivot('type')` | `type` = `gallery`/`file` |

## 8. Testes de migração de dados

| # | Cenário | Assert |
|---|---|---|
| MIG01 | `contents_id` preenchido | vira `attachable_type=content`, `attachable_id=<id>` |
| MIG02 | `path` = `storage/content/x/y.png` | vira `content/x/y.png` |
| MIG03 | `path` = `http://host/storage/answer/1/f.pdf` | vira `answer/1/f.pdf` (ou o registro é sinalizado para revisão manual) |
| MIG04 | `path` = `docs/f.pdf` | sinalizado: **não** pertence à tabela `archives` (é `obras_paralisadas`) — não deve ser migrado |
| MIG05 | `highlight` = `true` / `NULL` / `false` | `is_featured` = `true` / `false` / `false` (decisão E6) |
| MIG06 | `order` NULL | `position` = 0; reindexação posterior |
| MIG07 | Idempotência | rodar 2× não duplica linhas nem altera contagem |
| MIG08 | Relatório | script emite contagem de registros por `attachable_type` e de `path` irreconhecíveis |

## 9. Mocks e duplos

| Dependência | Estratégia |
|---|---|
| Storage | `Storage::fake('public')` |
| Intervenção de imagem (compressão) | Job `GenerateImageVariants` roda de forma síncrona (`QUEUE_CONNECTION=sync`) no teste; ou `Queue::fake()` + asserção do job. |
| Sentinel (auth) | `Sentinel::registerAndActivate()` + `Sentinel::authenticate()`, conforme `v6/tests/DashboardTestCase.php:93-118`. |
| Permissões | Criar papéis via `Sentinel::findRoleBySlug()` e `addPermission()`. |
| Uploads | `Illuminate\Http\UploadedFile::fake()->image('a.jpg', 100, 100)` / `->create('a.pdf', 100, 'application/pdf')`. |

## 10. Ambiente de teste

```dotenv
APP_ENV=testing
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_DATABASE=prefeituras_v7_test
FILESYSTEM_DISK=public
QUEUE_CONNECTION=sync
CACHE_DRIVER=array
SESSION_DRIVER=array
```

```bash
# preparar o banco de testes
php artisan migrate --env=testing --seed

# rodar
php artisan test --filter=Archive
```

## 11. Critérios de aceite da suíte

- [ ] **Nenhum** endpoint do Drive responde 2xx sem autenticação.
- [ ] Upload valida tipo, tamanho e a existência da categoria.
- [ ] Reordenação é atômica e escopada.
- [ ] Exclusão nunca deixa a página do portal quebrada.
- [ ] A migração de dados é idempotente e emite relatório de divergências.
- [ ] Cobertura de código das classes `Archive`, `ArchiveCategory` e
      `ArchiveService` ≥ 85%.
