// @vitest-environment node /** * S19 — `shareLink`: slug validation (format, reserved, duplicate), destination * validation (http/https only), slugify fallback, the fail-closed `published` * switch for anonymous reads, the slug-keyed public loader and the listing cache * tag busted by afterChange/afterDelete. */ import type { Payload } from 'payload' import { getPayload } from 'payload' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { revalidateTagMock, getCachedGlobalMock } = vi.hoisted(() => ({ revalidateTagMock: vi.fn(), getCachedGlobalMock: vi.fn(), })) // The collection hooks bust the Next cache tag; the loader reaches // `unstable_cache`. Both need the Next runtime, so mock it — the assertion // pins the tag name. vi.mock('next/cache', () => ({ revalidatePath: () => undefined, revalidateTag: revalidateTagMock, unstable_cache: (fn: unknown) => fn, })) // The site-default image fallback reads the shared `metadata` global; mocking // the read lets the test exercise it without writing a global the rest of the // suite (int and e2e share `teqo_wt19_test`) also consumes. vi.mock('@/utilities/globalReads', () => ({ getCachedGlobal: getCachedGlobalMock, })) import { SHARE_LINK_DESTINATION_INVALID_MESSAGE, SHARE_LINK_SLUG_DUPLICATE_MESSAGE, SHARE_LINK_SLUG_INVALID_MESSAGE, SHARE_LINK_SLUG_RESERVED_MESSAGE, } from '@/lib/shareLink' import config from '@/payload.config' import { getCachedPublishedShareLinkBySlug, resolveShareLinkOgImageUrl, } from '@/utilities/shareLinkReads' const TEST_PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64', ) let payload: Payload const createdIds = new Set() const createdMediaIds = new Set() type ShareLinkInput = { title: string slug: string destination: string description: string image?: number published?: boolean } const createShareLink = async (data: ShareLinkInput) => { const doc = await payload.create({ collection: 'shareLink', data }) createdIds.add(doc.id) return doc } const findAnonymous = (slug: string) => payload.find({ collection: 'shareLink', where: { slug: { equals: slug } }, depth: 0, limit: 1, overrideAccess: false, }) /** * Payload wraps a field `validate` message in a generic ValidationError; the * pt-BR message travels in `data.errors[].message`. */ const createErrorMessages = async (data: ShareLinkInput): Promise => { try { const doc = await payload.create({ collection: 'shareLink', data }) createdIds.add(doc.id) } catch (error) { const entries = (error as { data?: { errors?: Array<{ message?: string }> } }).data?.errors ?? [] return entries.map((entry) => entry.message ?? '') } return [] } describe('shareLink', () => { beforeAll(async () => { payload = await getPayload({ config: await config }) }) beforeEach(() => { revalidateTagMock.mockClear() getCachedGlobalMock.mockReset() // `resolveSiteMetadata` falls back to this env when the global has no URL. process.env.NEXT_PUBLIC_SITE_URL = 'https://site.test' }) afterAll(async () => { delete process.env.NEXT_PUBLIC_SITE_URL for (const id of createdIds) { await payload.delete({ collection: 'shareLink', id }).catch(() => undefined) } for (const id of createdMediaIds) { await payload.delete({ collection: 'media', id }).catch(() => undefined) } }) it('creates a draft by default and busts the shareLinks tag', async () => { const doc = await createShareLink({ title: 'Plenária da saúde', slug: 'plenaria-saude', destination: 'https://meet.google.com/abc-defg-hij', description: 'Participe da plenária sobre saúde.', }) expect(doc.published).toBe(false) expect(revalidateTagMock).toHaveBeenCalledWith('shareLinks') }) it('generates the slug from the title when empty', async () => { const doc = await createShareLink({ title: 'Plenária da Saúde em Feira', slug: '', destination: 'https://meet.google.com/abc-defg-hij', description: 'Participe.', }) expect(doc.slug).toBe('plenaria-da-saude-em-feira') }) it('trims the destination', async () => { const doc = await createShareLink({ title: 'Reunião', slug: 'reuniao-trim', destination: ' https://meet.google.com/abc-defg-hij ', description: 'Participe.', }) expect(doc.destination).toBe('https://meet.google.com/abc-defg-hij') }) it('rejects a reserved slug with a clear message', async () => { const messages = await createErrorMessages({ title: 'Corte', slug: 'corte', destination: 'https://meet.google.com/abc', description: 'x', }) expect(messages).toContain(SHARE_LINK_SLUG_RESERVED_MESSAGE) }) it.each([ ['Plenaria', 'plenaria'], ['plenaria da saude', 'plenaria-da-saude'], ['plenaria--feira', 'plenaria-feira'], ['plenaria.2026', 'plenaria-2026'], ])('normalizes the slug %s to %s', async (input, expected) => { const doc = await createShareLink({ title: 'Plenária', slug: input, destination: 'https://meet.google.com/abc', description: 'x', }) expect(doc.slug).toBe(expected) }) it('rejects a slug that resolves to nothing with a clear message', async () => { const messages = await createErrorMessages({ title: 'Plenária', slug: '---', destination: 'https://meet.google.com/abc', description: 'x', }) expect(messages).toContain(SHARE_LINK_SLUG_INVALID_MESSAGE) }) it('rejects a duplicate slug with a clear message', async () => { await createShareLink({ title: 'Primeira', slug: 'slug-unico-s19', destination: 'https://meet.google.com/abc', description: 'x', }) const messages = await createErrorMessages({ title: 'Segunda', slug: 'slug-unico-s19', destination: 'https://meet.google.com/def', description: 'y', }) expect(messages).toContain(SHARE_LINK_SLUG_DUPLICATE_MESSAGE) }) it.each(['meet.google.com/abc', 'ftp://example.com/x', 'javascript:alert(1)'])( 'rejects the invalid destination %s', async (destination) => { const messages = await createErrorMessages({ title: 'Plenária', slug: `destino-invalido-${Buffer.from(destination).toString('hex').slice(0, 12)}`, destination, description: 'x', }) expect(messages).toContain(SHARE_LINK_DESTINATION_INVALID_MESSAGE) }, ) it('hides drafts from anonymous reads and exposes them once published', async () => { const draft = await createShareLink({ title: 'Rascunho', slug: 'rascunho-s19', destination: 'https://meet.google.com/abc', description: 'x', }) expect((await findAnonymous('rascunho-s19')).totalDocs).toBe(0) expect(await getCachedPublishedShareLinkBySlug('rascunho-s19')()).toBeNull() await payload.update({ collection: 'shareLink', id: draft.id, data: { published: true } }) expect((await findAnonymous('rascunho-s19')).totalDocs).toBe(1) const loaded = await getCachedPublishedShareLinkBySlug('rascunho-s19')() expect(loaded?.id).toBe(draft.id) }) it('busts the tag on update and delete', async () => { const doc = await createShareLink({ title: 'Ciclo', slug: 'ciclo-s19', destination: 'https://meet.google.com/abc', description: 'x', }) revalidateTagMock.mockClear() await payload.update({ collection: 'shareLink', id: doc.id, data: { published: true } }) expect(revalidateTagMock).toHaveBeenCalledWith('shareLinks') revalidateTagMock.mockClear() await payload.delete({ collection: 'shareLink', id: doc.id }) createdIds.delete(doc.id) expect(revalidateTagMock).toHaveBeenCalledWith('shareLinks') }) it('absolutizes the image against the deployment origin and falls back to the site default', async () => { const media = await payload.create({ collection: 'media', data: { alt: 'Imagem do link' }, file: { data: TEST_PNG, mimetype: 'image/png', name: `share-link-int-${Date.now()}.png`, size: TEST_PNG.length, }, overrideAccess: true, }) createdMediaIds.add(media.id) const expectedUrl = `https://site.test${media.url}` const withImage = await createShareLink({ title: 'Com imagem', slug: 'imagem-configurada-s19', destination: 'https://meet.google.com/abc', description: 'x', image: media.id, }) const populated = await payload.findByID({ collection: 'shareLink', id: withImage.id, depth: 1, }) // The global carries the canonical domain (served elsewhere, e.g. the legacy // WordPress): the media proxy only exists on the deployment origin, so the // image URL must come from `NEXT_PUBLIC_SITE_URL`, never from `URL`. getCachedGlobalMock.mockReturnValue(async () => ({ URL: 'https://canonical.example' })) expect(await resolveShareLinkOgImageUrl(populated)).toBe(expectedUrl) const withoutImage = await createShareLink({ title: 'Sem imagem', slug: 'imagem-fallback-s19', destination: 'https://meet.google.com/abc', description: 'x', }) // No site default configured: no image at all — never a relative/broken one. expect(await resolveShareLinkOgImageUrl(withoutImage)).toBeNull() getCachedGlobalMock.mockReturnValue(async () => ({ URL: 'https://canonical.example', image: { url: '/api/media/file/site-default.png' }, })) expect(await resolveShareLinkOgImageUrl(withoutImage)).toBe( 'https://site.test/api/media/file/site-default.png', ) }) })