teqo-backup/tests/unit/contentPieceCirculation.unit.spec.ts
Francisco Solla f47c2645ac feat(C213): contadores anônimos de circulação por peça na Central de Conteúdos (Closes #1258)
Collection contentEvent (hidden, sem hooks, sem PII) + beacon público
POST /api/content-events fail-soft, download contado no servidor na rota
de mídia, agregação em uma query group-by e a coluna "Circulação" na lista
interna + painel na ficha (port classe-a-classe do artefato estendido,
crítica final do designer certificada).
2026-09-23 15:29:43 -03:00

125 lines
5.1 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import {
CONTENT_PIECE_CIRCULATION_HISTORY_LABEL,
CONTENT_PIECE_CIRCULATION_METRICS,
CONTENT_PIECE_CIRCULATION_NEVER_PUBLISHED_LABEL,
CONTENT_PIECE_CIRCULATION_ZERO_LABEL,
EMPTY_CONTENT_PIECE_CIRCULATION,
contentPieceCirculationFromRows,
isContentPieceCirculationEmpty,
resolveContentPieceCirculation,
showsContentPieceCirculationHistory,
type ContentPieceCirculationCounts,
} from '@/lib/contentPieceCirculation'
const counts = (
overrides: Partial<ContentPieceCirculationCounts> = {},
): ContentPieceCirculationCounts => ({ ...EMPTY_CONTENT_PIECE_CIRCULATION, ...overrides })
describe('content piece circulation vocabulary', () => {
it('maps the four event types to the four counters, once each', () => {
expect(CONTENT_PIECE_CIRCULATION_METRICS.map((metric) => metric.eventType)).toEqual([
'abertura',
'download',
'compartilhar_whatsapp',
'compartilhar_link',
])
expect(new Set(CONTENT_PIECE_CIRCULATION_METRICS.map((metric) => metric.key)).size).toBe(4)
// The detail names the action the fourth counter measures.
expect(
CONTENT_PIECE_CIRCULATION_METRICS.find((metric) => metric.key === 'link')?.detailLabel,
).toBe('Link copiado')
})
it('pins the honest literals of the design', () => {
expect(CONTENT_PIECE_CIRCULATION_ZERO_LABEL).toBe(
'0 abertura · 0 download · 0 WhatsApp · 0 link',
)
expect(CONTENT_PIECE_CIRCULATION_NEVER_PUBLISHED_LABEL).toBe('Sem dados — ainda não publicada')
expect(CONTENT_PIECE_CIRCULATION_HISTORY_LABEL).toBe(
'Histórico de quando esteve publicada · peça fora do ar',
)
})
})
describe('contentPieceCirculationFromRows', () => {
it('groups the aggregate rows per piece, ignoring unknown types and subjects', () => {
const byPieceId = contentPieceCirculationFromRows([
{ subjectId: '7', type: 'abertura', count: 3 },
{ subjectId: '7', type: 'download', count: 1 },
{ subjectId: '7', type: 'compartilhar_whatsapp', count: 2 },
{ subjectId: '8', type: 'compartilhar_link', count: 4 },
// Unknown event type and non-numeric subject (card model ids arrive with
// subjectType 'card', never through the piece mapper).
{ subjectId: '7', type: 'card_download', count: 9 },
{ subjectId: 'time-de-voce', type: 'download', count: 9 },
{ subjectId: '0', type: 'download', count: 9 },
])
expect(byPieceId.get(7)).toEqual({ opens: 3, downloads: 1, whatsapp: 2, link: 0 })
expect(byPieceId.get(8)).toEqual({ opens: 0, downloads: 0, whatsapp: 0, link: 4 })
expect(byPieceId.size).toBe(2)
})
it('normalizes hostile counts to non-negative integers', () => {
const byPieceId = contentPieceCirculationFromRows([
{ subjectId: '7', type: 'download', count: Number.NaN },
{ subjectId: '7', type: 'abertura', count: 2.7 },
{ subjectId: '8', type: 'download', count: -4 },
])
expect(byPieceId.get(7)).toEqual({ opens: 2, downloads: 0, whatsapp: 0, link: 0 })
expect(byPieceId.get(8)).toEqual({ opens: 0, downloads: 0, whatsapp: 0, link: 0 })
})
})
describe('resolveContentPieceCirculation', () => {
it('a failed read is unavailable, whatever the counts and publication say', () => {
expect(
resolveContentPieceCirculation({
counts: counts({ opens: 5 }),
hasBeenPublished: true,
unavailable: true,
}),
).toEqual({ state: 'unavailable' })
})
it('real counters win over everything — an unpublished piece keeps its history', () => {
expect(
resolveContentPieceCirculation({
counts: counts({ downloads: 1 }),
hasBeenPublished: true,
unavailable: false,
}),
).toEqual({ state: 'data', counts: counts({ downloads: 1 }) })
})
it('a published piece with no events shows zeros; a never published one does not', () => {
expect(resolveContentPieceCirculation({ hasBeenPublished: true, unavailable: false })).toEqual({
state: 'data',
counts: EMPTY_CONTENT_PIECE_CIRCULATION,
})
expect(resolveContentPieceCirculation({ hasBeenPublished: false, unavailable: false })).toEqual(
{ state: 'neverPublished' },
)
})
it('flags the historical counters of an unpublished piece only', () => {
const withHistory = { state: 'data', counts: counts({ opens: 2 }) } as const
const zeros = { state: 'data', counts: EMPTY_CONTENT_PIECE_CIRCULATION } as const
expect(showsContentPieceCirculationHistory(withHistory, false)).toBe(true)
expect(showsContentPieceCirculationHistory(withHistory, true)).toBe(false)
expect(showsContentPieceCirculationHistory(zeros, false)).toBe(false)
expect(showsContentPieceCirculationHistory({ state: 'neverPublished' }, false)).toBe(false)
expect(showsContentPieceCirculationHistory({ state: 'unavailable' }, false)).toBe(false)
})
it('knows an all-zero piece from a piece with any signal', () => {
expect(isContentPieceCirculationEmpty(EMPTY_CONTENT_PIECE_CIRCULATION)).toBe(true)
expect(isContentPieceCirculationEmpty(counts({ link: 1 }))).toBe(false)
})
})