teqo-backup/tests/unit/shareLink.unit.spec.ts
Francisco Solla dda55adc27 feat(S27): Central de Conteúdos pública — Peça voto pra Solla 1313
/conteudos + /conteudos/<slug>: catálogo filtrável (Tipo · Cidade · Região ·
Tema · Instituição + termo na URL canônica), play sob demanda (Vídeo/Áudio só
carregam no toque; Foto com preview leve), Baixar e Compartilhar do próprio
card com a mensagem de voto por tipo (wa.me sem destinatário, editável; "A
mídia" usa a folha nativa e cai em download onde o aparelho não suportar) e o
card personalizado como peça sintética do catálogo → /cards.

- leitura pública nova (contentPieceReads) cacheada na tag contentPieces com o
  gate where status=publicado + contentPieceIsPublic; a rota GET
  /conteudos/<slug>/midia reusa buildPrivateMediaResponse (range, disposition,
  private no-store, 404 uniforme) com download jorge-solla-1313-<slug>.<ext> e
  a allowlist inline do dono estendida (webp/gif/avif + áudios das peças)
- contrato público puro do catálogo (filtros/VM/download) + mensagens de voto
  (contentPieceShare); OG com fallback do global; conteudos entra nos slugs
  reservados; kill switch por peça vale na hora (lista, página, rodapé/selo);
  descoberta condicional no CampaignFooter e no header local
- UI portada classe-a-classe do design aprovado (cenas 01–08, com as cenas
  06–08 estendidas no trigger a); sem migration, sem Consent e sem access novo;
  contentMedia segue privada (sem segunda cópia de mídia)
- testes: 2 unit specs novos, 18 casos int (leitura pública + rota de mídia) e
  e2e frontendConteudos (5 casos); manifest e curado atualizados
- follow-up de engenharia: #1276 (docs/plans/escala-dry-pos-s27.md)

Closes #1255
2026-09-23 11:13:57 -03:00

149 lines
4.4 KiB
TypeScript

import { existsSync, readdirSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import {
isReservedShareLinkSlug,
isValidShareLinkDestination,
isValidShareLinkSlug,
normalizeShareLinkDescription,
SHARE_LINK_RESERVED_SLUGS,
SHARE_LINK_SLUG_PATTERN,
shareLinkPath,
} from '@/lib/shareLink'
describe('shareLink slug', () => {
it('accepts lowercase kebab-case slugs', () => {
expect(isValidShareLinkSlug('plenaria-saude-15-07')).toBe(true)
expect(isValidShareLinkSlug('meet')).toBe(true)
expect(isValidShareLinkSlug('a1')).toBe(true)
})
it.each([
'Plenaria',
'plenaria_saude',
'plenaria--saude',
'-plenaria',
'plenaria-',
'',
'a/b',
'a.b',
])('rejects %s', (value) => {
expect(isValidShareLinkSlug(value)).toBe(false)
expect(SHARE_LINK_SLUG_PATTERN.test(value)).toBe(false)
})
it('flags the reserved slugs literally', () => {
expect([...SHARE_LINK_RESERVED_SLUGS].sort()).toEqual(
[
'abaixo-assinado',
'admin',
'api',
'artigo',
'artigos',
'campanha',
'cards',
'conteudos',
'corte',
'evento',
'jingles',
'mandato-no-whatsapp',
'noticia',
'privacidade',
].sort(),
)
expect(isReservedShareLinkSlug('corte')).toBe(true)
expect(isReservedShareLinkSlug('plenaria-saude')).toBe(false)
})
it('builds the single-segment public path', () => {
expect(shareLinkPath('plenaria-saude')).toBe('/plenaria-saude')
})
})
describe('shareLink destination', () => {
it.each([
'https://meet.google.com/abc-defg-hij',
'http://example.com',
'https://example.com/path?x=1#y',
])('accepts %s', (value) => {
expect(isValidShareLinkDestination(value)).toBe(true)
})
it.each([
'meet.google.com/abc',
'ftp://example.com/file',
'javascript:alert(1)',
'data:text/html,x',
'mailto:x@example.com',
'http:foo',
'https:example.com',
'',
])('rejects %s', (value) => {
expect(isValidShareLinkDestination(value)).toBe(false)
})
})
describe('normalizeShareLinkDescription', () => {
it('collapses newlines and repeated spaces so the meta tag stays single-line', () => {
expect(normalizeShareLinkDescription(' Venha para a\n\nplenária de saúde ')).toBe(
'Venha para a plenária de saúde',
)
})
})
describe('reserved slugs drift', () => {
const ROUTE_FILES = new Set(['page.tsx', 'page.ts', 'route.ts', 'route.tsx'])
/** Any route file below `dir` (dynamic segments included) makes the segment a URL. */
const hasRouteFile = (dir: string): boolean => {
if (!existsSync(dir)) return false
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isFile() && ROUTE_FILES.has(entry.name)) return true
if (entry.isDirectory() && hasRouteFile(path.join(dir, entry.name))) return true
}
return false
}
const staticRouteSegments = (): string[] => {
const roots = ['src/app/(frontend)', 'src/app/(frontend)/(home)']
const segments = new Set<string>()
for (const root of roots) {
const absolute = path.resolve(process.cwd(), root)
if (!existsSync(absolute)) continue
for (const entry of readdirSync(absolute, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith('[') || entry.name.startsWith('('))
continue
if (hasRouteFile(path.join(absolute, entry.name))) segments.add(entry.name)
}
}
return [...segments]
}
/** The post type enum is private to the server-only `posts.ts`; read it as source. */
const postTypesOnDisk = (): string[] => {
const source = readFileSync(path.resolve(process.cwd(), 'src/utilities/posts.ts'), 'utf8')
const match = source.match(/const POST_TYPES = \[([^\]]+)\]/)
if (!match?.[1]) return []
return [...match[1].matchAll(/'([^']+)'/g)].map((entry) => entry[1]!)
}
it('reserves every static route segment at the root of the public site', () => {
const missing = staticRouteSegments().filter((segment) => !isReservedShareLinkSlug(segment))
expect(missing).toEqual([])
})
it('reserves every post type value (the `[type]` branch answers them first)', () => {
const types = postTypesOnDisk()
expect(types).toEqual(['noticia', 'campanha', 'artigo', 'evento'])
const missing = types.filter((type) => !isReservedShareLinkSlug(type))
expect(missing).toEqual([])
})
})