teqo/tests/int/campaignNotifications.int.spec.ts
Francisco Solla c3f10cce23 feat(C108): sino de notificações — abrir marca tudo como lido, painel limpo, modal central no desktop
openCampaignNotifications (lista + marca em 1 round trip) substitui
listCampaignNotifications + markAllCampaignNotificationsRead; markAllNotificationsRead
extraída (owner-scoped, idempotente, cap 200) com int test; bell vira Dialog
desktop / Drawer mobile (par C94), sem título/rodapé/botões, skeleton de
loading, safe-area, focus-visible, role=alert; readAt sai do contrato do item;
P4-D ledger atualizado (batch sem transação agora no hot path, self-healing).
2026-08-10 02:56:20 -03:00

203 lines
5.9 KiB
TypeScript

// @vitest-environment node
import { getPayload, type Payload } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import config from '@/payload.config'
import {
countUnreadNotifications,
loadNotificationList,
markAllNotificationsRead,
} from '@/utilities/notification/notificationList'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
let payload: Payload
const campaignFixtures = installCampaignFixtures({
getPayload: () => payload,
setPayload: (nextPayload) => {
payload = nextPayload
},
})
describe('notification access', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('denies anonymous reads', async () => {
await expect(
payload.find({ collection: 'notification', overrideAccess: false }),
).rejects.toThrow(/permissão|not allowed/i)
})
it('shows a recipient only their own notifications', async () => {
const fixtures = campaignFixtures()
const [owner, other] = await Promise.all([
fixtures.createCampaignUser('advisor'),
fixtures.createCampaignUser('advisor'),
])
const own = await payload.create({
collection: 'notification',
data: {
recipient: owner.id,
type: 'municipality_update',
payload: {
title: 'Teste — Município',
detail: 'Detalhe',
href: '/campanha/municipios/teste',
},
},
overrideAccess: true,
})
await payload.create({
collection: 'notification',
data: {
recipient: other.id,
type: 'new_supporter',
payload: {
title: 'Outro',
detail: 'Detalhe',
href: '/campanha/apoiadores',
},
},
overrideAccess: true,
})
const visible = await payload.find({
collection: 'notification',
depth: 0,
user: owner,
overrideAccess: false,
})
expect(visible.docs.map((doc) => doc.id)).toEqual([own.id])
})
it('reads the bell loaders through owner-scoped access (no bypass)', async () => {
const fixtures = campaignFixtures()
const [owner, other] = await Promise.all([
fixtures.createCampaignUser('advisor'),
fixtures.createCampaignUser('advisor'),
])
const own = await payload.create({
collection: 'notification',
data: {
recipient: owner.id,
type: 'municipality_update',
payload: { title: 'Sino — própria', detail: 'Detalhe', href: '/campanha' },
},
overrideAccess: true,
})
await payload.create({
collection: 'notification',
data: {
recipient: other.id,
type: 'new_supporter',
payload: { title: 'Sino — alheia', detail: 'Detalhe', href: '/campanha' },
},
overrideAccess: true,
})
// The loaders take the actor, not a recipientID — a caller cannot ask for
// someone else's inbox (Pass 4: the previous recipientID+bypass shape
// trusted every call site).
await expect(loadNotificationList(payload, owner)).resolves.toEqual([
expect.objectContaining({ id: own.id }),
])
await expect(countUnreadNotifications(payload, owner)).resolves.toBe(1)
await expect(countUnreadNotifications(payload, other)).resolves.toBe(1)
})
it("marks only the actor's own unread notifications as read (C108)", async () => {
const fixtures = campaignFixtures()
const [owner, other] = await Promise.all([
fixtures.createCampaignUser('advisor'),
fixtures.createCampaignUser('advisor'),
])
const ownUnread = await payload.create({
collection: 'notification',
data: {
recipient: owner.id,
type: 'municipality_update',
payload: { title: 'Própria — não lida', detail: 'Detalhe', href: '/campanha' },
},
overrideAccess: true,
})
const ownAlreadyRead = await payload.create({
collection: 'notification',
data: {
recipient: owner.id,
type: 'new_supporter',
payload: { title: 'Própria — já lida', detail: 'Detalhe', href: '/campanha' },
readAt: new Date().toISOString(),
},
overrideAccess: true,
})
const otherUnread = await payload.create({
collection: 'notification',
data: {
recipient: other.id,
type: 'invite_accepted',
payload: { title: 'Alheia — não lida', detail: 'Detalhe', href: '/campanha' },
},
overrideAccess: true,
})
const markedCount = await markAllNotificationsRead(payload, owner)
expect(markedCount).toBe(1)
const ownAfter = await payload.findByID({
collection: 'notification',
id: ownUnread.id,
depth: 0,
overrideAccess: true,
})
expect(ownAfter.readAt).not.toBeNull()
const readOwn = await payload.findByID({
collection: 'notification',
id: ownAlreadyRead.id,
depth: 0,
overrideAccess: true,
})
expect(readOwn.readAt).not.toBeNull()
const untouched = await payload.findByID({
collection: 'notification',
id: otherUnread.id,
depth: 0,
overrideAccess: true,
})
expect(untouched.readAt).toBeNull()
// Idempotent: a second pass touches nothing.
await expect(markAllNotificationsRead(payload, owner)).resolves.toBe(0)
await expect(countUnreadNotifications(payload, owner)).resolves.toBe(0)
})
})
describe('pushSubscription access', () => {
it('denies client-shaped subscription writes', async () => {
const fixtures = campaignFixtures()
const owner = await fixtures.createCampaignUser('advisor')
await expect(
payload.create({
collection: 'pushSubscription',
data: {
user: owner.id,
endpoint: 'https://push.example/1',
p256dh: 'key',
auth: 'auth',
consent: 1,
consentContentHash: 'hash',
consentedAt: new Date().toISOString(),
},
user: owner,
overrideAccess: false,
}),
).rejects.toThrow(/permissão|not allowed/i)
})
})