teqo-backup/tests/unit/webSpeech.unit.spec.ts

223 lines
8.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
canonicalWebSpeechUrl,
parseWebSpeechBatch,
parseWebSpeechFinding,
webSpeechAtFromPublishedAt,
webSpeechCreditLabel,
webSpeechDisplayTitle,
webSpeechDownloadFilename,
webSpeechMediaKind,
webSpeechPlatformLabel,
webSpeechSourceKey,
} from '@/lib/webSpeech'
// C215 — the pure web-speech contract: identity (sourceKey), canonical URL and
// the finding-batch validation that fails one entry without killing the batch.
describe('canonicalWebSpeechUrl', () => {
it('collapses every YouTube shape to the watch URL', () => {
expect(canonicalWebSpeechUrl('youtube', 'https://youtu.be/dQw4w9WgXcQ?si=track')).toBe(
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
)
expect(
canonicalWebSpeechUrl('youtube', 'https://www.youtube.com/shorts/dQw4w9WgXcQ?feature=share'),
).toBe('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
expect(
canonicalWebSpeechUrl('youtube', 'https://m.youtube.com/watch?v=dQw4w9WgXcQ&t=30s'),
).toBe('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
expect(canonicalWebSpeechUrl('youtube', 'https://www.youtube.com/live/dQw4w9WgXcQ')).toBe(
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
)
})
it('collapses Instagram post shapes and normalizes reels', () => {
expect(
canonicalWebSpeechUrl('instagram', 'https://www.instagram.com/reels/ABC123/?igsh=xyz'),
).toBe('https://www.instagram.com/reel/ABC123/')
expect(canonicalWebSpeechUrl('instagram', 'https://instagram.com/p/ABC123')).toBe(
'https://www.instagram.com/p/ABC123/',
)
})
it('drops tracking params, the fragment and the trailing slash for direct audio', () => {
expect(
canonicalWebSpeechUrl(
'radio',
'https://RadioBahia.com.br/audio/entrevista.mp3/?utm_source=share&id=7#t=1',
),
).toBe('https://radiobahia.com.br/audio/entrevista.mp3?id=7')
})
it('keeps an unparseable URL trimmed instead of inventing one', () => {
expect(canonicalWebSpeechUrl('audio', ' nao-e-url ')).toBe('nao-e-url')
})
})
describe('webSpeechSourceKey', () => {
it('prefers the external id and falls back to the canonical URL', () => {
expect(
webSpeechSourceKey({
platform: 'youtube',
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
externalId: 'dQw4w9WgXcQ',
}),
).toBe('web:youtube:dQw4w9WgXcQ')
expect(
webSpeechSourceKey({ platform: 'audio', url: 'https://radio.example/a.mp3?utm_source=x' }),
).toBe('web:audio:https://radio.example/a.mp3')
})
it('is stable across YouTube URL shapes (same identity, no duplicate row)', () => {
const short = webSpeechSourceKey({ platform: 'youtube', url: 'https://youtu.be/dQw4w9WgXcQ' })
const long = webSpeechSourceKey({
platform: 'youtube',
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90',
})
expect(short).toBe(long)
expect(short).toBe('web:youtube:https://www.youtube.com/watch?v=dQw4w9WgXcQ')
})
})
describe('webSpeechAtFromPublishedAt', () => {
it('normalizes date-only and converts an explicit timezone to Brasília', () => {
expect(webSpeechAtFromPublishedAt('2026-09-20')).toBe('2026-09-20T00:00')
// 14:35Z is 11:35 in America/Bahia (UTC-3, no DST).
expect(webSpeechAtFromPublishedAt('2026-09-20T14:35:00Z')).toBe('2026-09-20T11:35')
expect(webSpeechAtFromPublishedAt('2026-09-20T14:35:00-03:00')).toBe('2026-09-20T14:35')
expect(webSpeechAtFromPublishedAt('2026-09-20 14:35')).toBe('2026-09-20T14:35')
})
it('returns null for an unrecognized or impossible date', () => {
expect(webSpeechAtFromPublishedAt('20/09/2026')).toBeNull()
expect(webSpeechAtFromPublishedAt('2026-09-20lixo')).toBeNull()
expect(webSpeechAtFromPublishedAt('2026-99-99')).toBeNull()
expect(webSpeechAtFromPublishedAt('2026-09-20T25:00')).toBeNull()
})
})
describe('parseWebSpeechFinding', () => {
const validYoutube = {
platform: 'youtube',
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
publishedAt: '2026-09-20',
}
it('accepts a YouTube finding without mediaUrl', () => {
const parsed = parseWebSpeechFinding(validYoutube)
expect(parsed.ok).toBe(true)
})
it('requires mediaUrl for radio/audio and publishedAt everywhere', () => {
const radio = parseWebSpeechFinding({ ...validYoutube, platform: 'radio' })
expect(radio.ok).toBe(false)
if (!radio.ok) expect(radio.error).toContain('mediaUrl')
const noDate = parseWebSpeechFinding({ ...validYoutube, publishedAt: undefined })
expect(noDate.ok).toBe(false)
const badDate = parseWebSpeechFinding({ ...validYoutube, publishedAt: 'ontem' })
expect(badDate.ok).toBe(false)
})
it('rejects an unknown platform and a non-URL', () => {
expect(parseWebSpeechFinding({ ...validYoutube, platform: 'tiktok' }).ok).toBe(false)
expect(parseWebSpeechFinding({ ...validYoutube, url: 'nada' }).ok).toBe(false)
})
})
describe('parseWebSpeechBatch', () => {
it('validates only the envelope and keeps the findings raw', () => {
const parsed = parseWebSpeechBatch({ generatedAt: '2026-09-24T10:00:00Z', findings: [{}] })
expect(parsed.ok).toBe(true)
if (parsed.ok) {
expect(parsed.batch.generatedAt).toBe('2026-09-24T10:00:00Z')
expect(parsed.batch.findings).toEqual([{}])
}
})
it('fails closed on a malformed envelope', () => {
expect(parseWebSpeechBatch({}).ok).toBe(false)
expect(parseWebSpeechBatch({ findings: 'nope' }).ok).toBe(false)
})
})
describe('webSpeechPlatformLabel', () => {
it('owns the pt-BR labels the acervo shows', () => {
expect(webSpeechPlatformLabel('youtube')).toBe('YouTube')
expect(webSpeechPlatformLabel('radio')).toBe('Rádio')
})
// C216 — the pill of a row without a known platform is honest, not blank.
it('falls back for a missing or unknown platform (C216)', () => {
expect(webSpeechPlatformLabel(null)).toBe('Outra plataforma')
expect(webSpeechPlatformLabel(undefined)).toBe('Outra plataforma')
})
})
// C216 (closes the C215 S4 defer) — the download name of the mirrored file.
describe('webSpeechDownloadFilename', () => {
it('keeps the stored extension and the title in the download name', () => {
expect(
webSpeechDownloadFilename({
title: 'Entrevista sobre a saúde pública',
storedFilename: 'source.mp4',
}),
).toBe('Entrevista sobre a saúde pública.mp4')
})
it('strips control characters and path separators', () => {
expect(
webSpeechDownloadFilename({
title: 'Fala "sobre"\n o SUS/educação',
storedFilename: 'source.mp3',
}),
).toBe('Fala sobre o SUS educação.mp3')
})
it('caps the length and never leaves a trailing dot', () => {
const filename = webSpeechDownloadFilename({
title: `${'a'.repeat(300)}.`,
storedFilename: 'source.mp4',
})
expect(filename).not.toBeNull()
expect(filename?.endsWith('.mp4')).toBe(true)
expect(filename?.length).toBeLessThanOrEqual(120)
})
it('falls back to the stored filename when the title leaves nothing readable', () => {
expect(webSpeechDownloadFilename({ title: ' ', storedFilename: 'source.mp3' })).toBeNull()
expect(webSpeechDownloadFilename({ title: 'Fala', storedFilename: null })).toBeNull()
})
})
// C217 — the pure web-speech rules the cut shares with the detail/list VMs.
describe('webSpeechMediaKind', () => {
it('reads the mirrored mime: audio gets the native audio control', () => {
expect(webSpeechMediaKind('audio/mpeg')).toBe('audio')
expect(webSpeechMediaKind('audio/mp4')).toBe('audio')
expect(webSpeechMediaKind('video/mp4')).toBe('video')
expect(webSpeechMediaKind(null)).toBe('video')
expect(webSpeechMediaKind(undefined)).toBe('video')
})
})
describe('webSpeechDisplayTitle', () => {
it('prefers the platform title and degrades to the row id', () => {
expect(webSpeechDisplayTitle({ id: 9, title: ' Entrevista ' })).toBe('Entrevista')
expect(webSpeechDisplayTitle({ id: 9, title: ' ' })).toBe('Fala da internet #9')
expect(webSpeechDisplayTitle({ id: 9 })).toBe('Fala da internet #9')
})
})
describe('webSpeechCreditLabel', () => {
it('composes the platform with the channel and never claims a missing source', () => {
expect(webSpeechCreditLabel({ platform: 'youtube', channel: 'Canal do Solla' })).toBe(
'YouTube · Canal do Solla',
)
expect(webSpeechCreditLabel({ platform: 'radio', channel: ' ' })).toBe('Rádio')
expect(webSpeechCreditLabel({ platform: null, channel: 'Canal' })).toBeNull()
expect(webSpeechCreditLabel({})).toBeNull()
})
})