# A Cidade — Estratégia de Testes

> Versão analisada: `v6`. PHPUnit 6.5.14 / PHP 7.2.34 (verificado no container
> `v6_6f22e1bd-app-1`).

---

## 1. Estado atual da suíte

| Item | Realidade |
|---|---|
| Testes existentes | 31 arquivos `*Test.php` em `v6/tests/` — **todos** do Dashboard |
| Suítes declaradas | `Feature` (`v6/tests/Feature`) e `Unit` (`v6/tests/Unit`) — `v6/phpunit.xml:11-19` |
| Testes de A Cidade | **Nenhum** |
| Testes do portal público | **Nenhum** |
| Testes da view SQL `a_cidade` | **Nenhum** |
| Factories existentes | `HighlightFactory`, `ManualAreaFactory`, `ManualFactory`, `UserFactory` (`v6/database/factories/`) |
| Factories de `Content`, `CategoryContent`, `Archive`, `Link` | **Não existem** |

O harness de referência para testes que tocam banco e sessão é
`v6/tests/DashboardTestCase.php`, que usa `DatabaseTransactions`, cria usuário
Sentinel e ajusta `modulos.status`.

### 1.1 Comando exato para rodar

```bash
# dentro do container da aplicação (raiz do projeto: /home/cpaneluser/public_html)
docker exec -w /home/cpaneluser/public_html v6_6f22e1bd-app-1 php vendor/bin/phpunit

# apenas a suíte Unit
docker exec -w /home/cpaneluser/public_html v6_6f22e1bd-app-1 php vendor/bin/phpunit --testsuite Unit

# um arquivo específico
docker exec -w /home/cpaneluser/public_html v6_6f22e1bd-app-1 php vendor/bin/phpunit tests/Feature/ACidadePageTest.php

# filtrar por método
docker exec -w /home/cpaneluser/public_html v6_6f22e1bd-app-1 php vendor/bin/phpunit --filter testSlugDeOutraCategoriaNaoEhServido
```

Variáveis de ambiente usadas nos testes (`v6/phpunit.xml:25-30`):
`APP_ENV=testing`, `CACHE_DRIVER=array`, `SESSION_DRIVER=array`,
`QUEUE_DRIVER=sync`. **Não existe `.env.testing`** — o `DB_CONNECTION` e o
`DB_DATABASE` vêm do `.env` do ambiente. Os testes com `DatabaseTransactions`
rolem back, mas exigem um banco real (PostgreSQL), não SQLite.

> Verificado: `php` **não** está instalado no host; o PHP roda apenas dentro do
> container.

## 2. Estratégia

| Camada | Tipo | Ferramenta | Cobertura alvo |
|---|---|---|---|
| Unidade | `Unit` | PHPUnit + mocks | `embedVideo()`, `normalizeVideoLink()`, view SQL (via query), slug |
| Integração (feature HTTP) | `Feature` | Laravel `TestCase` + `DatabaseTransactions` | rota do portal, rotas do CMS, permissões, cliques |
| Views | `Feature` | asserção de HTML | presença dos slots, do breadcrumb, do iframe |
| Regressão manual | — | checklist | layout, manutenção automática |

Prioridade (do mais crítico ao menos):

1. **Filtragem por categoria** — o bug de segurança (conteúdo de outra
   categoria servido em `/a-cidade/{slug}`).
2. **Filtragem por status** — rascunho não pode ser publicado.
3. **404 real** para slug inexistente (hoje 200).
4. **Manutenção automática** quando o layout ativo não tem a view.
5. **Cliques** incrementados exatamente uma vez.
6. **Permissões do CMS** (papel sem `a-cidade.edit` não vê o botão de edição).
7. **View SQL** não lista conteúdo de outra categoria nem excluído.

## 3. Fixtures necessárias

### 3.1 Factories a criar

```php
// (ilustrativo) database/factories/CategoryContentFactory.php
$factory->define(App\Prefecture\CategoryContent::class, function (Faker $faker) {
    return [
        'name' => 'A Cidade',
        'slug' => 'a-cidade',
        'status_create' => 1,
        'status' => true,
    ];
});

// (ilustrativo) database/factories/ContentFactory.php
$factory->define(App\Prefecture\Content::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence(3),
        'slug' => $faker->unique()->slug,
        'text' => '<p>Texto institucional</p>',
        'clicks' => 0,
        'galery' => false,
        'status' => true,
        'category_contents_id' => function () {
            return factory(App\Prefecture\CategoryContent::class)->create()->id;
        },
    ];
});

// (ilustrativo) database/factories/ArchiveFactory.php
$factory->define(App\Prefecture\Archive::class, function (Faker $faker) {
    return [
        'title' => 'Imagem',
        'name' => 'imagem.jpg',
        'extension' => 'jpg',
        'path' => 'storage/content/a-cidade/1/destaque/imagem.jpg',
        'highlight' => 1,
        'contents_id' => function () {
            return factory(App\Prefecture\Content::class)->create()->id;
        },
    ];
});

// (ilustrativo) database/factories/LinkFactory.php
$factory->define(App\Prefecture\Link::class, function (Faker $faker) {
    return [
        'title' => 'Vídeo',
        'name' => 'Vídeo',
        'alt' => 'Vídeo',
        'link' => 'https://www.youtube.com/watch?v=YbJOTdZBX1g',
        'contents_id' => function () {
            return factory(App\Prefecture\Content::class)->create()->id;
        },
    ];
});
```

### 3.2 Configuração de layout nos testes

A view do portal depende de `configurations.key = 'layout'`. O helper
`getTemplateActived()` aborta 404 (e liga a manutenção) se
`portal.layouts.<layout>.home.index` não existir. O fixture precisa:

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

Layouts válidos para teste: `modelo1`..`modelo6` (todos com `home/index.blade.php`).
Layouts **inválidos** para testar o caminho de manutenção: `default`, `newsbit`.

### 3.3 Seed mínimo da view SQL

A view `a_cidade` é um objeto do banco: ela **não** é recriada por seed. Em
ambiente de teste limpo, é preciso criá-la:

```bash
docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 -c "
CREATE OR REPLACE VIEW a_cidade AS
SELECT row_number() OVER (ORDER BY c.created_at DESC) AS value,
    c.title AS titulo, c.id as id, c.slug as slug
FROM contents c
    JOIN category_contents cc ON cc.id = c.category_contents_id
WHERE c.deleted_at IS NULL AND cc.slug = 'a-cidade'
ORDER BY c.created_at DESC;"
```

Alternativa preferível: no `setUp()`, `DB::statement('CREATE OR REPLACE VIEW a_cidade AS ...')`
a partir do mesmo SQL da migration, e `DROP VIEW` no `tearDown()`.

## 4. Casos de teste

### 4.1 Página pública (`tests/Feature/ACidadePageTest.php`)

| # | Arrange | Act | Assert |
|---|---|---|---|
| T-1 | Conteúdo da categoria `a-cidade`, `status = true`, `clicks = 0`, layout `modelo1` | `GET /a-cidade/{slug}` | 200; HTML contém o título e o texto; `contents.clicks` = 1 |
| T-2 | idem | `GET /a-cidade/{slug}` duas vezes | `contents.clicks` = 2 |
| T-3 | Slug inexistente | `GET /a-cidade/nao-existe` | 200 **hoje** (view de erro); o teste marca a falha esperada para a v7 (404) |
| T-4 | **Conteúdo de outra categoria** (`noticias`) com slug `x` | `GET /a-cidade/x` | **Hoje: 200 com o conteúdo da notícia (FALHA — é o bug).** Teste de regressão: na v7 deve ser 404 |
| T-5 | Conteúdo da categoria `a-cidade` com `status = false` | `GET /a-cidade/{slug}` | **Hoje: 200 (FALHA — é o bug).** Na v7 deve ser 404 |
| T-6 | Conteúdo com `deleted_at` preenchido | `GET /a-cidade/{slug}` | 200 com a view de erro (o soft delete é respeitado pelo Eloquent) |
| T-7 | Conteúdo com `linksUnique->link` do YouTube | `GET /a-cidade/{slug}` | HTML contém `https://www.youtube.com/embed/YbJOTdZBX1g` |
| T-8 | Conteúdo com `archiveHighlight` e `galery = 1` + 2 arquivos de galeria | `GET /a-cidade/{slug}` | HTML contém `id="lightgallery"` e 2 `<li>` |
| T-9 | `configurations.layout = 'default'` | `GET /a-cidade/{slug}` | 404 **e** `configurations.pagina_em_manutencao` passa a `1` |
| T-10 | `configurations.layout = 'modelo3'`, conteúdo da categoria `a-cidade` | `GET /a-cidade/{slug}` | 200; o breadcrumb contém "A Cidade" |
| T-11 | Conteúdo da categoria `a-cidade` com `archiveHighlight` cujo arquivo não existe no disco | `GET /a-cidade/{slug}` | 200; **o `<img>` de destaque não aparece** (documenta o bug do `file_exist()` com path relativo) |
| T-12 | Conteúdo com `galery = 1` e ao menos um `archive` sem `highlight`; layout `modelo3` | `GET /a-cidade/{slug}` | **Hoje: 500 ("View [...] not found")** — o include do modelo3 aponta para `informacoes-prefeitura.galeria.index`. Teste de regressão: na v7 deve ser 200 com o lightGallery renderizado |

### 4.2 Listagem no CMS (`tests/Feature/ACidadeDatatableTest.php`)

| # | Arrange | Act | Assert |
|---|---|---|---|
| T-13 | Usuário autenticado com `a-cidade.index` | `GET /cms/datatable/a_cidade` | 200; JSON `data` com 1 item por conteúdo da categoria |
| T-14 | Conteúdo de outra categoria | idem | O item **não** aparece no JSON |
| T-15 | Conteúdo com `deleted_at` | idem | O item **não** aparece no JSON |
| T-16 | Conteúdo com `status = false` | idem | O item **aparece** (comportamento atual — registrar como dívida) |
| T-17 | Papel `super-admin` | idem | `edit = true`, `delete = true`, `active = true`, `inactivate = true` em cada linha |
| T-18 | Papel sem `a-cidade.edit` | idem | `edit = false` em cada linha |
| T-19 | Papel sem `a-cidade.delete` | idem | `delete = false` em cada linha |

### 4.3 Telas do CMS (`tests/Feature/ACidadeCmsTest.php`)

| # | Arrange | Act | Assert |
|---|---|---|---|
| T-20 | Operador com `a-cidade.index` | `GET /cms/conteudos/a-cidade` | 200; HTML contém `id="tabela_a_cidade"` |
| T-21 | Operador **sem** `a-cidade.index` | idem | Redirect (403/302) — o `check.route.permission` bloqueia |
| T-22 | Operador sem `a-cidade.create` | idem | HTML **não** contém o botão "Cadastrar Conteúdo" |
| T-23 | Operador sem `a-cidade.delete` | idem | HTML **não** contém o checkbox de seleção múltipla |
| T-24 | Operador sem `a-cidade.delete` | idem | HTML **não** contém o botão "Excluir Registro(s)" — **hoje o botão depende de `noticias.delete`**, então este teste **falha** e documenta o bug |
| T-25 | Operador com `a-cidade.create` | `GET /cms/conteudos/cadastrar/a-cidade` | 200; HTML contém `name="title"` e `name="text"` |
| T-26 | Operador com `a-cidade.edit` | `GET /cms/conteudos/editar/a-cidade/{id}` | 200; HTML contém `{{ url('a-cidade') }}/` e o link `route('a-cidade.index', $slug)` |
| T-27 | Operador autenticado | `POST /cms/conteudo` com `title`, `text`, `category_contents_id = 4` | 302; o conteúdo existe na base; `slug` gerado |
| T-28 | idem, com `title` duplicado | `POST /cms/conteudo` | slug recebe o sufixo `-<maxid+1>` |
| T-29 | idem, `title` vazio | `POST /cms/conteudo` | 302 de volta; erro de validação "Por favor, informe um título." |
| T-30 | idem, `text` vazio | `POST /cms/conteudo` | erro "Por favor, digite algum texto." |
| T-31 | idem, `archive_highlight` com PDF | `POST /cms/conteudo` | erro de mimes |
| T-32 | Conteúdo existente | `DELETE /cms/conteudo/delete/{id}` | 302; `contents.deleted_at` preenchido; o conteúdo **não** aparece mais na view `a_cidade` |
| T-33 | Conteúdo com `menu_items` apontando para ele | idem | os `menu_items` são removidos |

### 4.4 Unidade (`tests/Unit/`)

| # | Alvo | Assert |
|---|---|---|
| T-34 | `embedVideo('https://www.youtube.com/watch?v=YbJOTdZBX1g')` | `https://www.youtube.com/embed/YbJOTdZBX1g` |
| T-35 | `embedVideo('https://youtu.be/YbJOTdZBX1g')` | `https://www.youtube.com/embed/YbJOTdZBX1g` |
| T-36 | `embedVideo('https://vimeo.com/123456789')` | `https://player.vimeo.com/video/123456789` |
| T-37 | `embedVideo('https://www.facebook.com/somepage/videos/123')` | contém `facebook.com/plugins/video.php?href=` |
| T-38 | `embedVideo('https://exemplo.com/video')` | devolve a URL original (fallback) |
| T-39 | `embedVideo('')` | `''` |
| T-40 | `embedVideo(null)` | `''` |
| T-41 | `normalizeVideoLink('instagram.com/reel/ABC/?x=1')` | URL pública normalizada |
| T-42 | `normalizeVideoLink('YbJOTdZBX1g')` | `https://YbJOTdZBX1g` (esquema garantido) |
| T-43 | `Content::slugfy('A Cidade — História!')` | `a-cidade-historia` |
| T-44 | Query na view `a_cidade` com 2 conteúdos da categoria e 1 de outra | 2 linhas; a de outra categoria não aparece |

> `embedVideo()` e `normalizeVideoLink()` são funções **globais** em
> `v6/app/Helpers/functions.php`. O arquivo de helpers precisa ser carregado
> explicitamente no teste (o mesmo padrão já usado em
> `v6/tests/DashboardTestCase.php:13` para `appHelpers.php`):
> `require_once __DIR__ . '/../../app/Helpers/functions.php';`

## 5. Fixtures de view e seed

| Item | Arquivo | Finalidade |
|---|---|---|
| Conteúdo institucional de exemplo | `v6/database/seeds/ContentsTableSeeder.php:16-47` | Cria `historia`, `mapa`, `hino-oficial`, `fotos-da-cidade` com texto "Em breve...". |
| Categoria âncora | `v6/database/seeds/CategoryContentTableSeeder.php:30-34` | Garante `a-cidade`. |
| Recurso de permissão | `v6/database/seeds/ResourcesTableSeeder.php:80-88` | `resources.slug = 'a-cidade'`, `modulo_id = 13`. |
| Entrada de menu do CMS | `v6/database/seeds/MenuOrderSeeder.php:64-69` | `menu_orders.slug = 'a-cidade'`. |
| Módulo do CMS | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php:101-106` | `modulos.id = 13`, `status = 0`. |
| View SQL | `v6/database/migrations/2024_11_29_094836_update_view_a_cidade.php` | Definição canônica da view `a_cidade`. |

Nenhuma view Blade de teste é necessária: as asserções são feitas sobre o HTML
renderizado.

## 6. Checklist de regressão manual (v6)

| # | Passo | Resultado esperado |
|---|---|---|
| M-01 | `SELECT * FROM a_cidade;` | Só conteúdos da categoria `a-cidade`, sem excluídos, ordenados por `created_at DESC` |
| M-02 | `GET /a-cidade/historia` | Página institucional com título "História" |
| M-03 | `GET /a-cidade/historia` duas vezes | `contents.clicks` aumenta em 2 |
| M-04 | `GET /a-cidade/inexistente` | Página "CONTEÚDO NÃO ENCONTRADO." com HTTP 200 |
| M-05 | `GET /a-cidade/{slug-de-notícia}` | **Servido hoje** — comportamento a ser bloqueado |
| M-06 | `UPDATE configurations SET value='default' WHERE key='layout'` e `GET /a-cidade/historia` | HTTP 404 e `pagina_em_manutencao` = 1 |
| M-07 | Restaurar `layout` = `modelo1` e `GET /a-cidade/historia` | 200 (o valor de manutenção precisa ser resetado manualmente) |
| M-08 | `/cms/conteudos/a-cidade` como `administrador` | 200; grade com 4 linhas |
| M-09 | Verificar o menu lateral do CMS | **A Cidade não aparece** (`modulos.status = false`) |
| M-10 | Clicar em "Acessar" na grade | Abre `/a-cidade/{slug}` em nova aba |
| M-11 | Editar um conteúdo | "Link Permanente" mostra `https://<dominio>/a-cidade/` |
| M-12 | Excluir um conteúdo | Soft delete; some da listagem |

## 7. Cobertura atual do módulo

| Área | Cobertura |
|---|---|
| Portal público | 0% |
| CMS (listagem, CRUD, permissões) | 0% |
| Helpers (`embedVideo`, `normalizeVideoLink`) | 0% |
| View SQL `a_cidade` | 0% |
| Blade views | 0% |

**Meta mínima para a v7:** 100% dos casos T-1 a T-44 acima, com T-04, T-05,
T-12 e T-24 **corrigidos** (o teste deve passar porque o bug foi corrigido, e
não porque o comportamento foi documentado).

## 8. Ambientes

| Item | Valor |
|---|---|
| Container da aplicação | `v6_6f22e1bd-app-1` |
| Container do banco | `v6_6f22e1bd-postgres-1` |
| Banco | `prefeituras_v5` (conforme o `DB_DATABASE` do `.env` do ambiente) |
| Imagem PHP | `PHP 7.2.34 (cli)` |
| PHPUnit | `6.5.14` |
| Sobe/derruba | `docker compose -f v6/docker-compose.yml up -d` / `down` |
