mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
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).
203 lines
5.9 KiB
TypeScript
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)
|
|
})
|
|
})
|