# TV Câmara — Estratégia e Casos de Teste

> **Escopo:** módulo `tv-camara` na `v6`. O módulo **não possui** suíte de
> testes própria: não há arquivo de teste em `v6/tests/Feature/` ou
> `v6/tests/Unit/` que mencione `tv-camara` (verificado por busca textual).
> O que segue é o plano de testes a implementar, com os comandos exatos.

---

## 1. Estado atual

| Item | Situação |
|---|---|
| Testes do módulo | **Inexistentes** |
| Testes de dashboard (referência de convenção) | `v6/tests/Feature/Dashboard*Test.php`, base `v6/tests/DashboardTestCase.php` |
| Runner | PHPUnit (configurado em `v6/phpunit.xml`) |
| Banco de teste | PostgreSQL, com `DatabaseTransactions` rollback (padrão do `DashboardTestCase`) |
| Cache / sessão / fila no teste | `array` / `array` / `sync` (`v6/phpunit.xml:24-26`) |

## 2. Estratégia

| Camada | Ferramenta | Foco |
|---|---|---|
| Unit | PHPUnit + `Tests\TestCase` | `embedVideo()` para cada provedor; `str_slug`; normalização de data. |
| Feature (portal) | PHPUnit + `Tests\TestCase` + `DatabaseTransactions` | Filtros da listagem, paginação, página de detalhes, bloco da home. |
| Feature (CMS) | PHPUnit + `Tests\TestCase` + `DatabaseTransactions` | Autorização, Form Request, `store`/`update`/`setStatus`/`destroy`/`deleteByLote`. |
| Integração (banco) | `psql` em modo leitura | conferred de view SQL `tv_camara` e FK `archives_tv_camara_id_foreign`. |
| Manual | navegador | checagem de embed, aparência dos cards e do player. |

Convenções herdadas do repositório:

- Namespace `Tests\Feature` / `Tests\Unit`, arquivos com sufixo `Test.php`.
- Carregar o helper global explicitamente, como faz o
  `v6/tests/DashboardTestCase.php:16`:
  `require_once __DIR__ . '/../app/Library/Helper/appHelpers.php';`
  (necessário para `has_access()`).
- Isolar com `DatabaseTransactions` para não sujar o banco.

## 3. Fixtures necessárias

| Factory | Dados |
|---|---|
| `TvCamaraFactory` | `title` sequencial, `text`, `slug` único, `status` booleano, `link`, `publication_date` (com estados: passado, futuro, agora). |
| `ArchiveFactory` | `tv_camara_id`, `highlight = true`, `path` apontando para um arquivo real em `storage/`. |
| `UserFactory` (Sentinel) | Usuário com papel `super-admin` e usuários com papel limitado e permissões `tv-camara.*`. |
| Configuração | `Configuration` com `key = 'layout'` apontando para `modelo1`; `modulos` com `slug = 'tv-camara'` e `status = true`/`false`. |

Semântica do `slug` nas fixtures: para evitar colisão (bug A5), o `Factory`
deve gerar slugs únicos (por exemplo `video-1`, `video-2`).

## 4. Casos de teste — Portal (público)

| # | Arrange | Act | Assert | Teste sugerido |
|---|---|---|---|---|
| P1 | 3 vídeos `status = true`, `publication_date` no passado | `GET /tv-camara` | HTTP 200; os 3 títulos aparecem na página | `TvCamaraPublicIndexTest::testListaVideosAtivos` |
| P2 | 1 vídeo `status = false`, data passada | `GET /tv-camara` | HTTP 200; o título **não** aparece | `TvCamaraPublicIndexTest::testOcultaVideoDesativado` |
| P3 | 1 vídeo `status = true`, `publication_date` no futuro | `GET /tv-camara` | o título **não** aparece (filtro `published_at <= now()`) | `TvCamaraPublicIndexTest::testOcultaVideoFuturo` |
| P4 | 1 vídeo com `deleted_at` preenchido | `GET /tv-camara` | o título **não** aparece | `TvCamaraPublicIndexTest::testOcultaVideoExcluido` |
| P5 | 13 vídeos ativos no passado | `GET /tv-camara` | 12 títulos na 1ª página; link de paginação para a 2ª | `TvCamaraPublicIndexTest::testPaginacaoDe12` |
| P6 | 2 vídeos com `publication_date` distintas | `GET /tv-camara` | o mais recente aparece antes | `TvCamaraPublicIndexTest::testOrdenaPorDataDecrescente` |
| P7 | 1 vídeo ativo com `slug` conhecido | `GET /tv-camara/{slug}` | HTTP 200; título, data e iframe com `src` do embed | `TvCamaraPublicShowTest::testExibeDetalhe` |
| P8 | Nenhum vídeo com o `slug` | `GET /tv-camara/inexistente` | HTTP 404 | `TvCamaraPublicShowTest::testSlugInexistenteRetorna404` |
| P9 | 1 vídeo com `link` de YouTube | `GET /tv-camara/{slug}` | o HTML contém `https://www.youtube.com/embed/{id}` | `TvCamaraPublicShowTest::testEmbedYouTube` |
| P10 | 4 vídeos ativos | `GET /` (home) | no máximo 3 cards de TV Câmara | `TvCamaraHomeBlockTest::testHomeMostraNoMaximoTres` |
| P11 | `modulos.status = false` para `tv-camara` | `GET /` | o bloco "TV Câmara" não é renderizado | `TvCamaraHomeBlockTest::testBlocoOcultoComModuloDesativado` |
| P12 | 1 vídeo ativo e 1 desativado | `GET /` | só o ativo aparece no bloco | `TvCamaraHomeBlockTest::testBlocoIgnoraDesativados` |

## 5. Casos de teste — CMS

| # | Arrange | Act | Assert | Teste sugerido |
|---|---|---|---|---|
| C1 | Usuário sem login | `GET /cms/tv-camara` | redireciona para `login` | `TvCamaraCmsAccessTest::testExigeLogin` |
| C2 | Usuário sem `tv-camara.index` | `GET /cms/tv-camara` | redireciona com aviso de autorização | `TvCamaraCmsAccessTest::testExigePermissaoDeListagem` |
| C3 | `super-admin` | `GET /cms/tv-camara` | HTTP 200; tabela `tabela_tv-camara` presente | `TvCamaraCmsIndexTest::testListaCarregada` |
| C4 | Usuário sem `tv-camara.create` | `GET /cms/tv-camara` | o botão "Cadastrar TV Câmara" não aparece | `TvCamaraCmsIndexTest::testBotaoCadastrarExigePermissao` |
| C5 | Usuário sem `tv-camara.delete` (bug do slug) | `GET /cms/tv-camara` | o botão "Excluir Registro(s)" **não** aparece (hoje `tv-camaras.delete` nunca casa) | `TvCamaraCmsIndexTest::testBotaoExcluirLoteNaoApareceSemPermissao` |
| C6 | Payload sem `title` | `POST /cms/tv-camara/store` | 302 de volta; erros de validação com "O campo título é obrigatório." | `TvCamaraCmsStoreTest::testTituloObrigatorio` |
| C7 | Payload sem `link` | `POST /cms/tv-camara/store` | 302 de volta; erro "O campo link do vídeo é obrigatório." | `TvCamaraCmsStoreTest::testLinkObrigatorio` |
| C8 | Payload sem `publication_date` | `POST /cms/tv-camara/store` | 302 de volta; erro "O campo data de publicação é obrigatório." | `TvCamaraCmsStoreTest::testDataObrigatoria` |
| C9 | Payload válido com `title` de 121 caracteres | `POST /cms/tv-camara/store` | 302 de volta; erro de tamanho máximo | `TvCamaraCmsStoreTest::testTituloMaximo120` |
| C10 | Payload válido, `publication_date = 05/03/2024 14:30` | `POST /cms/tv-camara/store` | linha criada com `publication_date = 2024-03-05 14:30:00` | `TvCamaraCmsStoreTest::testConverteDataParaFormatoDoBanco` |
| C11 | Payload válido com título "Sessão Ordinária" | `POST /cms/tv-camara/store` | `slug = 'sessao-ordinaria'` | `TvCamaraCmsStoreTest::testGeraSlugDoTitulo` |
| C12 | Payload válido com imagem `archive_highlight` | `POST /cms/tv-camara/store` (multipart) | 1 registro em `tv_camaras`; 1 registro em `archives` com `tv_camara_id`, `highlight = true` e `path` iniciando com `storage/tv-camara/{id}/destaque/`; arquivo existe no disco | `TvCamaraCmsStoreTest::testGravaImagemDeDestaque` |
| C13 | 3 vídeos já ativos, novo vídeo com `status = 0` | `POST /cms/tv-camara/store` | o mais antigo dos 3 ativos fica com `status = false` | `TvCamaraCmsStoreTest::testDesativaTerceiroMaisAntigo` |
| C14 | 3 vídeos já ativos, novo vídeo com `status = 1` | `POST /cms/tv-camara/store` | os 3 anteriores permanecem ativos (comportamento atual; ver bug A4) | `TvCamaraCmsStoreTest::testNaoDesativaQuandoNovoEstaAtivo` |
| C15 | 4 vídeos já ativos, novo desativado | `POST /cms/tv-camara/store` | nenhum é desativado (`count($contents) == 3` é exato) | `TvCamaraCmsStoreTest::testNaoDesativaQuandoHaMaisDeTresAtivos` |
| C16 | Vídeo existente com imagem de destaque | `POST /cms/tv-camara/update/{id}` com nova imagem | `archives` continua com 1 destaque; `path` muda; arquivo antigo removido do storage | `TvCamaraCmsUpdateTest::testSubstituiImagemDeDestaque` |
| C17 | Vídeo existente sem imagem | `POST /cms/tv-camara/update/{id}` com imagem | 1 destaque criado com `highlight = true` | `TvCamaraCmsUpdateTest::testCriaDestaqueQuandoNaoExiste` |
| C18 | Vídeo existente | `POST /cms/tv-camara/update/{id}` com novo título | `slug` recalculado; `updated_at` alterado | `TvCamaraCmsUpdateTest::testAtualizaSlug` |
| C19 | Vídeo com `status = false` | `POST /cms/tv-camara/set-status` com `contents_id` | `status = true`; resposta `true` | `TvCamaraCmsSetStatusTest::testAtivaVideo` |
| C20 | Vídeo com `status = true` | `POST /cms/tv-camara/set-status` | `status = false`; resposta `true` | `TvCamaraCmsSetStatusTest::testDesativaVideo` |
| C21 | Vídeo existente | `DELETE /cms/tv-camara/delete/{id}` | `deleted_at` preenchido; linha continua na tabela; flash "Deletado com sucesso." | `TvCamaraCmsDestroyTest::testSoftDelete` |
| C22 | 3 vídeos existentes | `POST /cms/ajax/tv-camara/delete-por-lote` com `toDelete[]` | os 3 com `deleted_at` preenchido; resposta JSON `{"success": "Registros deletados!"}` | `TvCamaraCmsDestroyTest::testExclusaoEmLote` |
| C23 | Nenhum id no `toDelete` | `POST /cms/ajax/tv-camara/delete-por-lote` | flash "Nenhum ID foi informado para exclusão."; nenhum registro alterado | `TvCamaraCmsDestroyTest::testLoteSemIds` |
| C24 | 1 vídeo na tabela | `GET /cms/datatable/tv-camara` | JSON com `titulo`, `data_publicacao` (`dd/mm/aaaa`), `status_name` ("Ativado"/"Desativado") e `value` sequencial | `TvCamaraDatatableTest::testColunasDaViewSql` |
| C25 | 1 vídeo com `deleted_at` | `GET /cms/datatable/tv-camara` | o vídeo **não** aparece (a view filtra `deleted_at IS NULL`) | `TvCamaraDatatableTest::testViewIgnoraExcluidos` |

## 6. Casos de teste — Unit (`embedVideo`)

| # | Entrada | Saída esperada | Teste |
|---|---|---|---|
| U1 | `https://www.youtube.com/watch?v=YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` | `EmbedVideoTest::testYoutubeWatch` |
| U2 | `https://youtu.be/YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` | `EmbedVideoTest::testYoutubeShort` |
| U3 | `https://www.youtube.com/embed/YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` | `EmbedVideoTest::testYoutubeEmbed` |
| U4 | `https://vimeo.com/123456789` | `https://player.vimeo.com/video/123456789` | `EmbedVideoTest::testVimeo` |
| U5 | `https://www.facebook.com/watch/?v=123` | `https://www.facebook.com/plugins/video.php?href=...&show_text=false` | `EmbedVideoTest::testFacebook` |
| U6 | `https://www.instagram.com/reel/ABC123/?x=1` | `https://www.instagram.com/reel/ABC123/embed/` | `EmbedVideoTest::testInstagram` |
| U7 | `YbJOTdZBX1g` (ID cru) | `https://YbJOTdZBX1g` (fallback; **não** reproduz — bug A12) | `EmbedVideoTest::testIdCruNaoConverte` |
| U8 | `www.youtube.com/watch?v=YbJOTdZBX1g` (sem esquema) | `https://www.youtube.com/embed/YbJOTdZBX1g` | `EmbedVideoTest::testNormalizaEsquema` |
| U9 | `https://exemplo.com/video.mp4` | devolvido sem alteração (fallback) | `EmbedVideoTest::testUrlDesconhecida` |
| U10 | `''` (string vazia) | `''` | `EmbedVideoTest::testLinkVazio` |

## 7. Casos de teste — Banco de dados (leitura)

| # | Consulta | Resultado esperado |
|---|---|---|
| B1 | `\d tv_camaras` | 10 colunas conforme `02-MODELAGEM.md`; PK em `id`. |
| B2 | `\d tv_camara` | view com 6 colunas: `value`, `titulo`, `id`, `data_publicacao`, `status_name`, `status`. |
| B3 | `select pg_get_viewdef('tv_camara'::regclass, true)` | definição com `row_number()`, `to_char(...,'DD/MM/YYYY')` e `WHERE deleted_at IS NULL`. |
| B4 | `select * from information_schema.table_constraints where constraint_name = 'archives_tv_camara_id_foreign'` | FK de `archives.tv_camara_id` para `tv_camaras(id)` com `ON DELETE CASCADE`. |

## 8. Comandos exatos

```bash
# 1. Suíte completa (ambiente v6)
cd v6
./vendor/bin/phpunit

# 2. Apenas a suíte do módulo (após criar os arquivos)
cd v6
./vendor/bin/phpunit --testsuite Feature --filter TvCamara
./vendor/bin/phpunit --testsuite Unit --filter EmbedVideo

# 3. Arquivo específico
cd v6
./vendor/bin/phpunit tests/Feature/TvCamaraPublicIndexTest.php

# 4. Teste isolado por nome de método
cd v6
./vendor/bin/phpunit --filter testDesativaTerceiroMaisAntigo

# 5. Verificação da view SQL e da FK (somente leitura)
docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 -c "\d tv_camara"
docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 -c "select pg_get_viewdef('tv_camara'::regclass, true);"

# 6. Suíte no Laravel moderno (v7), com Pest ou PHPUnit
php artisan test --filter=TvCamara
```

## 9. Checklist de regressão manual (após qualquer mudança)

1. Habilitar o módulo em `/cms/modulos` e confirmar que o bloco "TV Câmara"
   aparece na home.
2. Cadastrar um vídeo do YouTube e conferir o player na página de detalhes.
3. Cadastrar um vídeo com **ID cru** e confirmar o comportamento atual
   (miniatura aparece, player em branco) — antes de corrigir, registre como
   bug conhecido.
4. Subir 4 vídeos e observar se a home continua mostrando 3 e se a listagem
   pública mantém a paginação de 12.
5. Excluir um vídeo e confirmar que ele some da listagem, da home e do
   DataTable do CMS, mas continua no banco com `deleted_at` preenchido.
6. Alternar o status e confirmar o reflexo no portal.
