mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
148 lines
5.2 KiB
TypeScript
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()
|
|
})
|
|
})
|