mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { randomUUID } from 'node:crypto'
|
|
import type { Payload } from 'payload'
|
|
import { getPayload } from 'payload'
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
import { matchMunicipalityMentions } from '@/lib/speechGazetteer'
|
|
import { INTERNET_SPEECH_MEDIA_SLUG } from '@/lib/webSpeech'
|
|
import config from '@/payload.config'
|
|
import { upsertSpeechBundle, type SpeechImportBundle } from '@/utilities/speech/speechImport'
|
|
import {
|
|
loadSpeechAcervoPageData,
|
|
loadSpeechDetailPageData,
|
|
loadWebSpeechAcervoPageData,
|
|
loadWebSpeechDetailPageData,
|
|
loadWebSpeechTitleForActor,
|
|
SpeechNotFoundError,
|
|
} from '@/utilities/speech/speechPageData'
|
|
|
|
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
|
import { speechBundleFixture } from '../helpers/speechBundleFixture'
|
|
import {
|
|
createInternetSpeechMedia,
|
|
WEB_SPEECH_MP3_BYTES,
|
|
WEB_SPEECH_MP4_BYTES,
|
|
} from '../helpers/webSpeechMediaFixture'
|
|
|
|
// C216 — the "Falas na internet" loaders over the real Payload boundary: the
|
|
// list reads only web rows, the facets answer over the web catalog, the detail
|
|
// carries the private media routes and the Câmara detail never serves a web row
|
|
// (and vice versa). The private routes themselves are e2e territory; here the
|
|
// view model contract is the target.
|
|
|
|
let payload: Payload
|
|
const createdSourceKeys = new Set<string>()
|
|
const createdMediaIds = new Set<number>()
|
|
|
|
const campaignFixtures = installCampaignFixtures({
|
|
getPayload: () => payload,
|
|
setPayload: (nextPayload) => {
|
|
payload = nextPayload
|
|
},
|
|
})
|
|
|
|
const createMedia = async (bytes: Buffer, filename: string): Promise<number> => {
|
|
const mediaId = await createInternetSpeechMedia(payload, bytes, filename)
|
|
createdMediaIds.add(mediaId)
|
|
return mediaId
|
|
}
|
|
|
|
const webBundle = (
|
|
overrides: Partial<SpeechImportBundle> = {},
|
|
sourceKey = `web:test:${randomUUID()}`,
|
|
): SpeechImportBundle => {
|
|
createdSourceKeys.add(sourceKey)
|
|
return {
|
|
...speechBundleFixture(),
|
|
sourceKey,
|
|
origin: 'web',
|
|
platform: 'youtube',
|
|
externalId: sourceKey,
|
|
sourceUrl: `https://www.youtube.com/watch?v=${encodeURIComponent(sourceKey)}`,
|
|
title: 'Fala publicada na internet',
|
|
channel: 'Canal do teste',
|
|
// Câmara-only coordinates never travel on a web row.
|
|
legislature: null,
|
|
type: null,
|
|
phase: null,
|
|
eventId: null,
|
|
audioId: null,
|
|
excerptTMs: null,
|
|
vodPlaybackUrl: null,
|
|
vodDownloadUrl: null,
|
|
officialTextUrl: null,
|
|
officialTranscript: null,
|
|
summary: null,
|
|
presidingOfficer: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
const createWebSpeech = async (overrides: Partial<SpeechImportBundle> = {}): Promise<number> => {
|
|
const bundle = webBundle(overrides)
|
|
await upsertSpeechBundle(payload, bundle)
|
|
const found = await payload.find({
|
|
collection: 'speech',
|
|
where: { sourceKey: { equals: bundle.sourceKey } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
const speech = found.docs[0]
|
|
if (!speech) throw new Error('web speech fixture was not created')
|
|
return speech.id
|
|
}
|
|
|
|
const createCamaraSpeech = async (overrides: Partial<SpeechImportBundle> = {}): Promise<number> => {
|
|
const bundle = speechBundleFixture(overrides)
|
|
createdSourceKeys.add(bundle.sourceKey)
|
|
await upsertSpeechBundle(payload, bundle)
|
|
const found = await payload.find({
|
|
collection: 'speech',
|
|
where: { sourceKey: { equals: bundle.sourceKey } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
const speech = found.docs[0]
|
|
if (!speech) throw new Error('Câmara speech fixture was not created')
|
|
return speech.id
|
|
}
|
|
|
|
const createUsers = async () => {
|
|
const fixtures = campaignFixtures()
|
|
const [communicator, coordinator, candidate, advisor, leader] = await Promise.all([
|
|
fixtures.createCampaignUser('communicator'),
|
|
fixtures.createCampaignUser('coordinator'),
|
|
fixtures.createCampaignUser('candidate'),
|
|
fixtures.createCampaignUser('advisor'),
|
|
fixtures.createCampaignUser('leader'),
|
|
])
|
|
return { communicator, coordinator, candidate, advisor, leader }
|
|
}
|
|
|
|
describe('web speech acervo (C216)', () => {
|
|
beforeAll(async () => {
|
|
payload = await getPayload({ config: await config })
|
|
})
|
|
|
|
afterAll(async () => {
|
|
for (const sourceKey of createdSourceKeys) {
|
|
const found = await payload.find({
|
|
collection: 'speech',
|
|
where: { sourceKey: { equals: sourceKey } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
const speech = found.docs[0]
|
|
if (speech) {
|
|
await payload.delete({
|
|
collection: 'speech',
|
|
id: speech.id,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
})
|
|
}
|
|
}
|
|
for (const mediaId of createdMediaIds) {
|
|
await payload
|
|
.delete({
|
|
collection: INTERNET_SPEECH_MEDIA_SLUG,
|
|
id: mediaId,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
})
|
|
.catch(() => undefined)
|
|
}
|
|
})
|
|
|
|
it('lists only web rows: the Câmara speech with the same marker stays invisible', async () => {
|
|
const runId = randomUUID().slice(0, 8)
|
|
const marker = `internet${runId}`
|
|
const webId = await createWebSpeech({
|
|
segments: [{ startSeconds: 0, endSeconds: 3, text: `A fala ${marker} na internet` }],
|
|
})
|
|
const camaraId = await createCamaraSpeech({
|
|
segments: [{ startSeconds: 0, endSeconds: 3, text: `A fala ${marker} na Câmara` }],
|
|
})
|
|
|
|
const { communicator } = await createUsers()
|
|
const data = await loadWebSpeechAcervoPageData(payload, communicator, { q: marker })
|
|
|
|
expect(data.totalDocs).toBe(1)
|
|
expect(data.rows.map((row) => row.id)).toEqual([webId])
|
|
expect(data.rows[0]?.excerpt.parts.map((part) => part.text).join('')).toContain(marker)
|
|
|
|
// And the Câmara list does not pick the web row up.
|
|
const camara = await loadSpeechAcervoPageData(payload, communicator, { q: marker })
|
|
expect(camara.rows.map((row) => row.id)).toEqual([camaraId])
|
|
})
|
|
|
|
it('filters by topic, year, duration and cited municipality over the web catalog', async () => {
|
|
const id = await createWebSpeech({
|
|
speechAt: '2021-04-12T00:00',
|
|
durationSeconds: 90,
|
|
facets: {
|
|
topics: ['igualdade-racial'],
|
|
scopes: ['brasil'],
|
|
municipalities: matchMunicipalityMentions('Feira de Santana'),
|
|
people: [],
|
|
programs: [],
|
|
projects: [],
|
|
classifiedBy: 'gazetteer',
|
|
},
|
|
})
|
|
|
|
const { communicator } = await createUsers()
|
|
const municipality = await payload.find({
|
|
collection: 'municipality',
|
|
where: { slug: { equals: 'feira-de-santana' } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
const municipalityId = municipality.docs[0]?.id
|
|
if (!municipalityId) throw new Error('seeded municipality missing')
|
|
|
|
const data = await loadWebSpeechAcervoPageData(payload, communicator, {
|
|
topic: 'igualdade-racial',
|
|
year: '2021',
|
|
duration: 'curta',
|
|
municipality: String(municipalityId),
|
|
})
|
|
|
|
expect(data.rows.map((row) => row.id)).toContain(id)
|
|
expect(data.filterOptions.years).toContain(2021)
|
|
// The web source has no Fase facet.
|
|
expect(data.filterOptions.phases).toEqual([])
|
|
})
|
|
|
|
it('orders by publication date and gates the duration orders', async () => {
|
|
const runId = randomUUID().slice(0, 8)
|
|
const marker = `ordem${runId}`
|
|
const older = await createWebSpeech({
|
|
speechAt: '2020-01-10T00:00',
|
|
durationSeconds: 90,
|
|
title: `Mais antiga ${marker}`,
|
|
segments: [{ startSeconds: 0, endSeconds: 3, text: `A fala ${marker} antiga` }],
|
|
})
|
|
const newer = await createWebSpeech({
|
|
speechAt: '2026-01-10T00:00',
|
|
durationSeconds: null,
|
|
title: `Mais recente ${marker}`,
|
|
segments: [{ startSeconds: 0, endSeconds: 3, text: `A fala ${marker} recente` }],
|
|
})
|
|
|
|
const { communicator } = await createUsers()
|
|
const byRecency = await loadWebSpeechAcervoPageData(payload, communicator, { q: marker })
|
|
expect(byRecency.rows.map((row) => row.id)).toEqual([newer, older])
|
|
|
|
// A duration order only lists rows with a measured duration.
|
|
const byDuration = await loadWebSpeechAcervoPageData(payload, communicator, {
|
|
q: marker,
|
|
sort: 'duracao_maior',
|
|
})
|
|
expect(byDuration.state.sort).toBe('duracao_maior')
|
|
expect(byDuration.rows.map((row) => row.id)).toEqual([older])
|
|
})
|
|
|
|
it('expands a theme only over the web rows and keeps the evidence (C192)', async () => {
|
|
const runId = randomUUID().slice(0, 8)
|
|
const marker = `temaweb${runId}`
|
|
const id = await createWebSpeech({
|
|
segments: [
|
|
{
|
|
startSeconds: 0,
|
|
endSeconds: 3,
|
|
text: `Garantir o atendimento universal ${marker} na rede pública`,
|
|
},
|
|
],
|
|
})
|
|
|
|
const { coordinator } = await createUsers()
|
|
const expansionCalls: string[] = []
|
|
const data = await loadWebSpeechAcervoPageData(
|
|
payload,
|
|
coordinator,
|
|
{ q: 'defesa do SUS', mode: 'tema' },
|
|
async (theme) => {
|
|
expansionCalls.push(theme)
|
|
return { terms: [`atendimento universal ${marker}`] }
|
|
},
|
|
)
|
|
|
|
expect(expansionCalls).toEqual(['defesa do SUS'])
|
|
expect(data.themeUnavailable).toBe(false)
|
|
expect(data.themeApplied).toBe(true)
|
|
const row = data.rows.find((item) => item.id === id)
|
|
expect(row).toBeDefined()
|
|
expect(row?.excerpt.parts.some((part) => part.highlighted)).toBe(true)
|
|
})
|
|
|
|
it('carries the private routes, the platform and the origin attribution in the detail', async () => {
|
|
const mediaId = await createMedia(WEB_SPEECH_MP3_BYTES, 'source.mp3')
|
|
const id = await createWebSpeech({
|
|
platform: 'radio',
|
|
title: 'Entrevista na rádio',
|
|
channel: 'Rádio Metrópole',
|
|
sourceUrl: 'https://radio.example/entrevista',
|
|
speechAt: '2026-09-21T00:00',
|
|
mirroredMedia: mediaId,
|
|
segments: [{ startSeconds: 0, endSeconds: 4, text: 'A saúde pública baiana' }],
|
|
})
|
|
|
|
const { communicator } = await createUsers()
|
|
const view = await loadWebSpeechDetailPageData(payload, communicator, id, 'saúde')
|
|
|
|
expect(view.title).toBe('Entrevista na rádio')
|
|
expect(view.platform).toEqual({ value: 'radio', label: 'Rádio' })
|
|
expect(view.channel).toBe('Rádio Metrópole')
|
|
expect(view.sourceUrl).toBe('https://radio.example/entrevista')
|
|
expect(view.dateLabel).toBe('21/09/2026')
|
|
expect(view.mediaKind).toBe('audio')
|
|
expect(view.fileHref).toBe(`/campanha/comunicacao/acervo/internet/${id}/arquivo`)
|
|
expect(view.downloadHref).toBe(`/campanha/comunicacao/acervo/internet/${id}/arquivo?download=1`)
|
|
expect(view.segments[0]?.parts.some((part) => part.highlighted)).toBe(true)
|
|
})
|
|
|
|
it('points the cover at the private route only when a thumbnail was captured', async () => {
|
|
const thumbnailId = await createMedia(WEB_SPEECH_MP4_BYTES, 'thumb.mp4')
|
|
const withCover = await createWebSpeech({ thumbnail: thumbnailId })
|
|
const withoutCover = await createWebSpeech({})
|
|
|
|
const { communicator } = await createUsers()
|
|
const data = await loadWebSpeechAcervoPageData(payload, communicator, {})
|
|
const row = (id: number) => data.rows.find((item) => item.id === id)
|
|
|
|
expect(row(withCover)?.thumbnailUrl).toBe(
|
|
`/campanha/comunicacao/acervo/internet/${withCover}/capa`,
|
|
)
|
|
expect(row(withoutCover)?.thumbnailUrl).toBeNull()
|
|
})
|
|
|
|
it('never serves one origin from the other origin detail loader', async () => {
|
|
const webId = await createWebSpeech({})
|
|
const camaraId = await createCamaraSpeech({})
|
|
const { communicator } = await createUsers()
|
|
|
|
await expect(
|
|
loadSpeechDetailPageData(payload, communicator, webId, undefined, async () => null),
|
|
).rejects.toBeInstanceOf(SpeechNotFoundError)
|
|
await expect(
|
|
loadWebSpeechDetailPageData(payload, communicator, camaraId),
|
|
).rejects.toBeInstanceOf(SpeechNotFoundError)
|
|
await expect(loadWebSpeechTitleForActor(payload, communicator, camaraId)).resolves.toBeNull()
|
|
})
|
|
|
|
it('allows communicator/coordinator/candidate and denies advisor/leader', async () => {
|
|
const id = await createWebSpeech({})
|
|
const { communicator, coordinator, candidate, advisor, leader } = await createUsers()
|
|
|
|
for (const reader of [communicator, coordinator, candidate]) {
|
|
const data = await loadWebSpeechAcervoPageData(payload, reader, {})
|
|
expect(data.rows.map((row) => row.id)).toContain(id)
|
|
await expect(loadWebSpeechDetailPageData(payload, reader, id)).resolves.toBeDefined()
|
|
}
|
|
|
|
for (const denied of [advisor, leader]) {
|
|
await expect(loadWebSpeechAcervoPageData(payload, denied, {})).rejects.toThrow(/permissão/i)
|
|
await expect(loadWebSpeechDetailPageData(payload, denied, id)).rejects.toThrow(/permissão/i)
|
|
}
|
|
})
|
|
})
|