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

148 lines
5.2 KiB
TypeScript

// @vitest-environment node
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
CONTENT_EVENT_ENDPOINT,
sendCardDownloadEvent,
sendContentPieceEvent,
} from '@/lib/contentEvents'
afterEach(() => {
vi.unstubAllGlobals()
})
describe('ContentEvent collection contract', () => {
it('carries the subject only — no visitor identifier — and never hooks the public cache', async () => {
const { ContentEvent } = await import('@/collections/ContentEvent')
const fieldNames = ContentEvent.fields.map((field) => ('name' in field ? field.name : ''))
expect(fieldNames).toEqual(['type', 'subjectType', 'subjectId', 'variant'])
for (const forbidden of ['ip', 'useragent', 'cookie', 'session', 'visitor', 'fingerprint']) {
expect(fieldNames.some((name) => name.toLowerCase().includes(forbidden))).toBe(false)
}
// A write must never bust the public `contentPieces` listing tag.
expect(ContentEvent.hooks ?? {}).toEqual({})
expect(ContentEvent.admin?.hidden).toBeTypeOf('function')
// No access predicate grants a create; the writer bypasses explicitly.
const createAccess = ContentEvent.access?.create
expect(createAccess).toBeTypeOf('function')
const decision = await (
createAccess as (args: { req: { user: null } }) => boolean | Promise<boolean>
)({ req: { user: null } })
expect(decision).toBe(false)
})
})
describe('sendContentPieceEvent', () => {
it('prefers sendBeacon with a JSON blob and does not touch fetch', async () => {
const sendBeacon = vi.fn().mockReturnValue(true)
const fetchMock = vi.fn()
vi.stubGlobal('navigator', { sendBeacon })
vi.stubGlobal('fetch', fetchMock)
sendContentPieceEvent('abertura', 'fim-da-escala-6x1')
expect(sendBeacon).toHaveBeenCalledTimes(1)
const [endpoint, blob] = sendBeacon.mock.calls[0]!
expect(endpoint).toBe(CONTENT_EVENT_ENDPOINT)
expect(blob).toBeInstanceOf(Blob)
expect((blob as Blob).type).toBe('application/json')
await expect((blob as Blob).text()).resolves.toBe(
JSON.stringify({ type: 'abertura', pieceSlug: 'fim-da-escala-6x1' }),
)
expect(fetchMock).not.toHaveBeenCalled()
})
it('falls back to a keepalive fetch when the beacon refuses the body', async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 }))
vi.stubGlobal('navigator', { sendBeacon: vi.fn().mockReturnValue(false) })
vi.stubGlobal('fetch', fetchMock)
sendContentPieceEvent('compartilhar_link', 'solla-no-sus')
expect(fetchMock).toHaveBeenCalledWith(
CONTENT_EVENT_ENDPOINT,
expect.objectContaining({
method: 'POST',
keepalive: true,
body: JSON.stringify({ type: 'compartilhar_link', pieceSlug: 'solla-no-sus' }),
}),
)
})
it('never throws when the beacon and the fetch both fail', async () => {
const fetchMock = vi.fn().mockRejectedValue(new Error('offline'))
vi.stubGlobal('navigator', {
sendBeacon: vi.fn().mockImplementation(() => {
throw new Error('beacon exploded')
}),
})
vi.stubGlobal('fetch', fetchMock)
expect(() => sendContentPieceEvent('download', 'peca')).not.toThrow()
await Promise.resolve()
vi.stubGlobal('fetch', undefined)
expect(() => sendContentPieceEvent('download', 'peca')).not.toThrow()
})
it('reaches the fetch fallback when there is no navigator at all', () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 }))
vi.stubGlobal('navigator', undefined)
vi.stubGlobal('fetch', fetchMock)
sendContentPieceEvent('download', 'peca')
expect(fetchMock).toHaveBeenCalledTimes(1)
})
})
describe('sendCardDownloadEvent', () => {
it('carries the model id and the state-deputy slug on the picker models', async () => {
const sendBeacon = vi.fn().mockReturnValue(true)
vi.stubGlobal('navigator', { sendBeacon })
sendCardDownloadEvent('minha-colinha', 'julio')
expect(sendBeacon).toHaveBeenCalledTimes(1)
const [endpoint, blob] = sendBeacon.mock.calls[0]!
expect(endpoint).toBe(CONTENT_EVENT_ENDPOINT)
await expect((blob as Blob).text()).resolves.toBe(
JSON.stringify({
type: 'download',
subjectType: 'card',
cardModelId: 'minha-colinha',
stateDeputySlug: 'julio',
}),
)
})
it('omits the slug key on models without a picker (null or undefined)', async () => {
const sendBeacon = vi.fn().mockReturnValue(true)
vi.stubGlobal('navigator', { sendBeacon })
sendCardDownloadEvent('eu-sou-solla', null)
const [, blob] = sendBeacon.mock.calls[0]!
await expect((blob as Blob).text()).resolves.toBe(
JSON.stringify({ type: 'download', subjectType: 'card', cardModelId: 'eu-sou-solla' }),
)
})
it('never throws when the beacon and the fetch both fail', async () => {
vi.stubGlobal('navigator', {
sendBeacon: vi.fn().mockImplementation(() => {
throw new Error('beacon exploded')
}),
})
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('offline')))
expect(() => sendCardDownloadEvent('time-do-estadual', 'julio')).not.toThrow()
await Promise.resolve()
vi.stubGlobal('fetch', undefined)
expect(() => sendCardDownloadEvent('time-do-estadual', 'julio')).not.toThrow()
})
})