# Sessões da Câmara Municipal — Estratégia de Testes

> Módulo `sessoes` na `v6`.
> Ambiente de testes: PHPUnit 7 (embutido no Laravel 5.5), banco PostgreSQL de
> teste, `DatabaseTransactions`.

---

## 1. Situação atual

| Item | Estado |
|---|---|
| Testes existentes para o módulo `sessoes` | **Nenhum**. `v6/tests/Feature/` tem 27 arquivos, todos do Dashboard; `v6/tests/Unit/` está vazio. |
| Suítes declaradas | `Feature` (`./tests/Feature`) e `Unit` (`./tests/Unit`) em `v6/phpunit.xml`. |
| Base de teste | `Tests\TestCase` (`v6/tests/TestCase.php`) e `Tests\DashboardTestCase` (`v6/tests/DashboardTestCase.php`), que já sabe criar usuários Sentinel, roles e permissões. |
| Ambiente | `APP_ENV=testing`, `CACHE_DRIVER=array`, `SESSION_DRIVER=array`, `QUEUE_DRIVER=sync` (`v6/phpunit.xml`). |

O `DashboardTestCase` é a base a ser reaproveitada: os métodos `createUser()`,
`createRole()` e `setModules()` já resuelvem a autenticação Sentinel e a
ativação de módulos, exatamente o que o módulo `sessoes` exige.

## 2. Estratégia

| Camada | Tipo | Foco |
|---|---|---|
| Portal (público) | Feature | Filtro de publicação, paginação, ordenação, detalhe, 404, galeria, embed. |
| CMS (AdminLTE) | Feature | Autorização por permissão, validação, criação/edição, exclusão em lote, DataTable. |
| Domínio | Unit | `embedVideo()`, normalização de URL, formatação de data, escopo de publicação. |
| Banco | Integração | View SQL `sessoes` coerente com `contents`. |

Nomes de arquivo propostos:

```
tests/Feature/Portal/SessionIndexTest.php
tests/Feature/Portal/SessionShowTest.php
tests/Feature/Admin/AdminSessionTest.php
tests/Feature/Admin/SessionDatatableTest.php
tests/Unit/EmbedVideoTest.php
tests/Unit/SessionScopeTest.php
tests/Feature/SessionViewSyncTest.php
```

## 3. Fixtures necessárias

Nenhuma factory do Laravel existe para `contents`. Recomenda-se criar
`database/factories/ContentFactory.php` (a criar) com os estados abaixo:

| Estado | Dados |
|---|---|
| `published()` | `status = true`, `publication_date = now()->subHour()`, `category_contents_id` = raiz `sessoes`. |
| `draft()` | `status = false`, `publication_date = now()->subHour()`. |
| `scheduled()` | `status = true`, `publication_date = now()->addWeek()` (data futura). |
| `withGallery()` | `galery = true` + 3 `archives` com `highlight = null` e extensões `jpg`. |
| `withCover()` | 1 `archive` com `highlight = true`. |
| `withVideo()` | 1 `link` com `link = 'https://www.youtube.com/watch?v=YbJOTdZBX1g'`. |

Helpers reutilizáveis no `DashboardTestCase` ou em uma trait
`CreatesSessions`:

```php
protected function sessionCategory()
{
    return \App\Prefecture\CategoryContent::firstOrCreate(
        ['slug' => 'sessoes'],
        ['name' => 'Sessões', 'status_create' => 1]
    );
}

protected function createSession(array $attributes = [])
{
    $category = $this->sessionCategory();

    return \App\Prefecture\Content::create(array_merge([
        'clicks'              => 0,
        'title'               => 'Sessão de Teste',
        'text'                => '<p>Texto da sessão</p>',
        'slug'                => 'sessao-de-teste-' . uniqid(),
        'publication_date'    => date('Y-m-d H:i:s', strtotime('-1 hour')),
        'status'              => true,
        'galery'              => false,
        'category_contents_id' => $category->id,
    ], $attributes));
}
```

**Fixture de view:** a view SQL `sessoes` precisa existir no banco de teste.
Como ela é criada por migration, o banco de teste precisa rodar
`php artisan migrate` (ou o CI precisa aplicar
`v6/database/migrations/2024_09_30_004936_create_view_sessoes.php`).

**Fixture de layout:** o controller do portal aborta 404 se a chave
`configurations.layout` não apontar para um tema com `home/index.blade.php`.
Nos testes, fixar `layout = 'modelo5'`:

```php
\DB::table('configurations')->where('key', 'layout')->update(['value' => 'modelo5']);
```

**Fixture de módulo:** para aparecer no menu do CMS,
`modulos.status` precisa ser 1 para o slug `sessoes`
(`DashboardTestCase::setModules(['sessoes' => true])`).

## 4. Casos de teste — portal (Feature)

### 4.1 `SessionIndexTest` — `GET /sessoes`

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | 3 sessões publicadas com `publication_date` no passado, títulos "A", "B", "C". | `GET /sessoes` | A resposta é 200; o HTML contém "A", "B" e "C". |
| P2 | 1 sessão `status = false` (rascunho) com data passada. | `GET /sessoes` | O HTML **não** contém o título do rascunho. |
| P3 | 1 sessão `status = true` com `publication_date` no futuro. | `GET /sessoes` | O HTML **não** contém o título da sessão futura. |
| P4 | 15 sessões publicadas no passado. | `GET /sessoes` | A paginação mostra 12 itens na primeira página e links para a segunda. |
| P5 | 15 sessões publicadas, com datas distintas. | `GET /sessoes` | A ordem exibida é da data mais recente para a mais antiga. |
| P6 | Nenhuma sessão. | `GET /sessoes` | Resposta 200; aparece "Nenhuma registro encontrado." |
| P7 | 1 sessão com slug `x` na categoria `sessoes` e outra com o **mesmo slug** em outra categoria. | `GET /sessoes` | Só a da categoria `sessoes` aparece. |
| P8 | Configuração `layout = 'default'`. | `GET /sessoes` | Resposta 404 com a mensagem `view_inexistente`. |
| P9 | Sessão publicada com `galery = true` e 2 arquivos. | `GET /sessoes` | Resposta 200; o card usa `getMedFile(caminho-da-imagem)` para a miniatura. |

### 4.2 `SessionShowTest` — `GET /sessoes/{slug}`

| # | Arrange | Act | Assert |
|---|---|---|---|
| S1 | Sessão publicada com título, texto, destaque e galeria `galery = true`. | `GET /sessoes/{slug}` | 200; contém o título, o texto, a imagem de destaque e a galeria. |
| S2 | Sessão com `galery = false` e 3 arquivos. | `GET /sessoes/{slug}` | 200; a galeria **não** é renderizada (ausência do `id="lightgallery"`). |
| S3 | Sessão com `galery = true` e 3 arquivos. | `GET /sessoes/{slug}` | 200; o `id="lightgallery"` está presente. |
| S4 | Sessão com `links.link = 'https://www.youtube.com/watch?v=YbJOTdZBX1g'`. | `GET /sessoes/{slug}` | O HTML contém `src="https://www.youtube.com/embed/YbJOTdZBX1g"`. |
| S5 | Sessão **sem** link. | `GET /sessoes/{slug}` | O HTML não contém `iframe-video`. |
| S6 | Slug inexistente. | `GET /sessoes/slug-inexistente` | Resposta 404. |
| S7 | Conteúdo com o slug procurado, mas em outra categoria. | `GET /sessoes/{slug}` | Resposta 404 (`firstOrFail` após `whereHas`). |
| S8 | Sessão em rascunho com slug conhecido. | `GET /sessoes/{slug}` | **Comportamento atual (bug B1):** responde 200. O teste deve documentar esse desvio até a correção. |

## 5. Casos de teste — CMS (Feature)

Base: `DashboardTestCase::createUser('super-admin')` ou uma role com
permissões específicas.

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | Usuário `super-admin` logado. | `GET /cms/conteudos/sessoes` | 200; a view `prefecture.contents.sessoes.index` é renderizada; o título é "Lista de Sessões". |
| C2 | Usuário sem a permissão `sessoes.index`. | `GET /cms/conteudos/sessoes` | Redireciona para a rota de login ou para o dashboard, com aviso. |
| C3 | Role com `sessoes.create`. | `GET /cms/conteudos/sessoes` | O botão "Cadastrar Sessão" aparece. |
| C4 | Role sem `sessoes.delete`. | `GET /cms/conteudos/sessoes` | O checkbox de exclusão em lote e o botão de excluir não aparecem. |
| C5 | `POST /cms/conteudo` com `category_slug = sessoes`, `title` vazio. | Submit | 302 de volta com o erro "Por favor, informe um título." |
| C6 | `POST /cms/conteudo` com `category_slug = sessoes`, `title` com 151 caracteres. | Submit | Erro de validação (limite 150). |
| C7 | `POST /cms/conteudo` com `category_slug = sessoes`, `text` vazio. | Submit | Erro "Por favor, digite algum texto." |
| C8 | `POST /cms/conteudo` com `category_slug = sessoes`, `publication_date` vazio. | Submit | Erro "Por favor, informe a data de publicação." |
| C9 | `POST /cms/conteudo` válido com `link` de 3000 caracteres. | Submit | Erro de validação (limite 2999). |
| C10 | `POST /cms/conteudo` válido com `archive_highlight` de 3 MB `.txt`. | Submit | Erro de mime (permitidos apenas `jpg,jpeg,png,gif,webp`). |
| C11 | `POST /cms/conteudo` válido com título "Sessão 12ª Extraordinária". | Submit | 302; o `contents` criado tem `category_contents_id` da raiz `sessoes`, `slug = 'sessao-12-extraordinaria'` e `status = true`. |
| C12 | Sessão existente, `PUT /cms/conteudo/{id}` alterando o título. | Submit | O `slug` **permanece** o original. |
| C13 | `POST /cms/conteudo` válido com `link` do YouTube. | Submit | A linha em `links` tem `contents_id` da sessão e `link` preenchido. |
| C14 | Sessão existente, `DELETE /cms/conteudo/delete/{id}`. | Request | 302; `contents.deleted_at` preenchido (soft delete) e a linha some da view `sessoes`. |
| C15 | Duas sessões selecionadas, `POST /cms/conteudo/deleteAll`. | Request | Ambas têm `deleted_at` preenchido. |
| C16 | `GET /cms/datatable/sessoes` com 3 sessões (1 rascunho). | Request | JSON com 3 linhas; a coluna `status_name_sessoes` traz "Rascunho" na linha do rascunho. |
| C17 | Role sem `sessoes.edit` e `sessoes.delete`. | `GET /cms/datatable/sessoes` | As flags `edit` e `delete` vêm `false`. |

## 6. Casos de teste — Unit

### 6.1 `EmbedVideoTest` — `v6/app/Helpers/functions.php:607`

| # | Entrada | Saída esperada |
|---|---|---|
| U1 | `https://www.youtube.com/watch?v=YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` |
| U2 | `https://youtu.be/YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` |
| U3 | `https://www.youtube.com/embed/YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` |
| U4 | `https://vimeo.com/123456789` | `https://player.vimeo.com/video/123456789` |
| U5 | `https://www.facebook.com/somepage/videos/101500000000` | `https://www.facebook.com/plugins/video.php?href={url}&show_text=false` |
| U6 | `https://www.instagram.com/reel/ABC123/` | `https://www.instagram.com/reel/ABC123/embed/` |
| U7 | `YbJOTdZBX1g` (sem esquema) | `https://www.youtube.com/embed/YbJOTdZBX1g` |
| U8 | `https://example.com/video.mp4` | `https://example.com/video.mp4` (fallback) |
| U9 | `''` | `''` |
| U10 | `null` | `''` |

### 6.2 `SessionScopeTest` (equivalente ao filtro do controller)

| # | Arrange | Act | Assert |
|---|---|---|---|
| US1 | 1 sessão publicada com data passada, 1 rascunho, 1 futura. | Aplicar o filtro equivalente a `SessionsController@index`. | Retorna apenas a sessão publicada. |
| US2 | 30 sessões publicadas. | `->count()` | 30 (o filtro não pagina). |

## 7. Teste de integração da view SQL

`tests/Feature/SessionViewSyncTest.php`:

| # | Arrange | Act | Assert |
|---|---|---|---|
| V1 | 2 sessões (1 publicada, 1 rascunho). | `DB::table('sessoes')->get()` | 2 linhas; `status_name_sessoes` = "Publicado" e "Rascunho". |
| V2 | 1 sessão com `publication_date = '2024-05-01 14:30:00'`. | `DB::table('sessoes')->first()` | `data_publicacao` = `01/05/2024 às 14:30`. |
| V3 | Sessão com `deleted_at` preenchido. | `DB::table('sessoes')->get()` | A linha não aparece (a view filtra `deleted_at IS NULL`). |
| V4 | Sessão em categoria filha de `sessoes`. | `DB::table('sessoes')->get()` | A linha **não** aparece (comportamento atual — bug B3). |
| V5 | 3 sessões com datas distintas. | `DB::table('sessoes')->orderBy('value')->get()` | A coluna `value` vai de 1 a 3 na ordem `publication_date DESC`. |

## 8. Como rodar

Todos os comandos são executados a partir do diretório `v6`.

```bash
# Suíte completa
cd /home/one/projects/maxima/v7/v6
./vendor/bin/phpunit

# Apenas a suíte Feature
./vendor/bin/phpunit --testsuite Feature

# Apenas a suíte Unit
./vendor/bin/phpunit --testsuite Unit

# Um arquivo específico
./vendor/bin/phpunit tests/Feature/Portal/SessionIndexTest.php

# Um caso específico (filtro por método)
./vendor/bin/phpunit --filter test_index_lists_only_published_sessions tests/Feature/Portal/SessionIndexTest.php

# Testes do módulo de sessões (vários arquivos)
./vendor/bin/phpunit tests/Feature/Portal tests/Feature/Admin/SessionDatatableTest.php tests/Unit/EmbedVideoTest.php
```

### 8.1 Preparar o banco de teste

```bash
cd /home/one/projects/maxima/v7/v6
cp .env.example .env
php artisan key:generate
php artisan migrate --env=testing
```

> O `migrate` **inclui** a migration `v6/database/migrations/2024_09_30_004936_create_view_sessoes.php`,
> indispensable para os testes de integração da view (seção 7).

### 8.2 Rodar dentro do container (ambiente oficial)

```bash
docker exec -w /home/cpaneluser/public_html v6_6f22e1bd-app-1 ./vendor/bin/phpunit --filter Session
```

## 9. Checklist de cobertura

| Comportamento documentado | Caso(s) |
|---|---|
| Só publicações vigentes aparecem | P1, P2, P3, US1 |
| Paginação de 12 | P4 |
| Ordenação por data desc | P5, V5 |
| Detalhe por slug restrito à categoria | S6, S7 |
| Galeria só com o flag ativo | S2, S3, P9 |
| Vídeo em *embed* | S4, S5, U1..U10 |
| Validações do formulário | C5..C10 |
| Slug gerado e preservado | C11, C12 |
| Exclusão lógica e em lote | C14, C15, V3 |
| Permissões do CMS | C1..C4, C17 |
| View SQL coerente com `contents` | V1..V5 |
| 404 por layout inválido | P8 |
| Widget da home | Cobertura ausente: criar `SessionHomeWidgetTest` para garantir os 4 itens mais recentes no `modelo5`. |
