teqo-backup/tests/unit/campaignPageActorGate.unit.spec.ts
Francisco Solla 448f3845a2 feat(C194): biblioteca de reels na vertical Comunicação (#1153)
Lista publicados-only com capas 9:16, detalhe com player 9:16 e painel de
downloads (vídeo sem áudio primário; com áudio, mp3 e .srt quando existem;
transcrição em texto) e kill switch reversível: despublicar tira da lista e
bloqueia a mídia (C193 decision B), republicar reativa e re-carimba publishedAt.

Fiação: gate communicationCatalog (rename do speechCatalog), 3º sub-item da
vertical no nav, rule da lista no chrome, paths e loader próprios; sem
migration, sem Consent, sem tocar Media.read ou a rota de mídia do C193.

Design tier: DEGRADED — crítica final do designer indisponível no tier
primário (limite de uso); requer sign-off humano antes de push/PR.
2026-09-18 21:54:37 -03:00

138 lines
5.1 KiB
TypeScript

// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
redirect: vi.fn(),
getCampaignUser: vi.fn(),
getCampaignUserWithAvatar: vi.fn(),
}))
vi.mock('next/navigation', () => ({
redirect: mocks.redirect,
}))
vi.mock('@/utilities/campaignAuth', () => ({
getCampaignUser: mocks.getCampaignUser,
getCampaignUserWithAvatar: mocks.getCampaignUserWithAvatar,
}))
vi.mock('@/utilities/access/shared', () => ({
advisorEditingAccess: (user: { role: string; visibility?: string; editing?: string }) => {
if (user.role !== 'advisor') return 'none'
if (user.editing === 'somente_leitura') return 'none'
return 'carteira'
},
}))
import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
type Actor = {
role: 'coordinator' | 'candidate' | 'communicator' | 'advisor' | 'leader'
visibility?: 'carteira' | 'tudo'
editing?: 'carteira' | 'tudo' | 'somente_leitura'
email: string
}
const actor = (
role: Actor['role'],
profile?: { visibility?: Actor['visibility']; editing?: Actor['editing'] },
): Actor => ({
role,
email: `${role}@example.com`,
...profile,
})
describe('requireCampaignPageActor — gate: writable (C142)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('lets a coordinator through the writable gate (C142 regression: advisorEditingAccess returns "none" for non-advisors)', async () => {
mocks.getCampaignUser.mockResolvedValue(actor('coordinator'))
const user = await requireCampaignPageActor({ gate: 'writable' })
expect(user.role).toBe('coordinator')
expect(mocks.redirect).not.toHaveBeenCalled()
})
it('lets a candidate through the writable gate', async () => {
mocks.getCampaignUser.mockResolvedValue(actor('candidate'))
const user = await requireCampaignPageActor({ gate: 'writable' })
expect(user.role).toBe('candidate')
expect(mocks.redirect).not.toHaveBeenCalled()
})
it('redirects an advisor with Edição somente_leitura away from write pages', async () => {
mocks.getCampaignUser.mockResolvedValue(actor('advisor', { editing: 'somente_leitura' }))
await requireCampaignPageActor({ gate: 'writable', redirectTo: '/campanha' })
expect(mocks.redirect).toHaveBeenCalledWith('/campanha')
})
it('lets an advisor with carteira editing through the writable gate', async () => {
mocks.getCampaignUser.mockResolvedValue(actor('advisor', { editing: 'carteira' }))
const user = await requireCampaignPageActor({ gate: 'writable' })
expect(user.role).toBe('advisor')
expect(mocks.redirect).not.toHaveBeenCalled()
})
it('blocks a leader (non-staff) on the writable gate', async () => {
mocks.getCampaignUser.mockResolvedValue(actor('leader'))
await requireCampaignPageActor({ gate: 'writable' })
expect(mocks.redirect).toHaveBeenCalled()
})
})
describe('requireCampaignPageActor — gate: communicationCatalog (C154, renamed by C194)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('lets the three communication roles through', async () => {
for (const role of ['communicator', 'coordinator', 'candidate'] as const) {
mocks.getCampaignUser.mockResolvedValue(actor(role))
const user = await requireCampaignPageActor({ gate: 'communicationCatalog' })
expect(user.role).toBe(role)
expect(mocks.redirect).not.toHaveBeenCalled()
}
})
it('sends the advisor to the campaign home', async () => {
mocks.getCampaignUser.mockResolvedValue(actor('advisor'))
await requireCampaignPageActor({ gate: 'communicationCatalog' })
expect(mocks.redirect).toHaveBeenCalledWith('/campanha')
})
it('sends the leader to their contacts tool', async () => {
mocks.getCampaignUser.mockResolvedValue(actor('leader'))
await requireCampaignPageActor({ gate: 'communicationCatalog' })
expect(mocks.redirect).toHaveBeenCalledWith(LEADER_CONTACTS_HOME)
})
})
describe('requireCampaignPageActor — withAvatar option', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('loads the actor through the avatar getter (perfil shell needs a populated avatarUrl)', async () => {
mocks.getCampaignUserWithAvatar.mockResolvedValue(actor('coordinator'))
const user = await requireCampaignPageActor({ withAvatar: true })
expect(user.role).toBe('coordinator')
expect(mocks.getCampaignUserWithAvatar).toHaveBeenCalledTimes(1)
expect(mocks.getCampaignUser).not.toHaveBeenCalled()
expect(mocks.redirect).not.toHaveBeenCalled()
})
it('redirects to login when the avatar getter finds no session', async () => {
mocks.getCampaignUserWithAvatar.mockResolvedValue(null)
// Real `next/navigation` redirect never returns (throws NEXT_REDIRECT);
// the helper reads `user.role` right after, so mimic that here.
mocks.redirect.mockImplementationOnce(() => {
throw new Error('NEXT_REDIRECT')
})
await expect(requireCampaignPageActor({ withAvatar: true })).rejects.toThrow('NEXT_REDIRECT')
expect(mocks.redirect).toHaveBeenCalledWith('/campanha/login')
expect(mocks.getCampaignUser).not.toHaveBeenCalled()
})
})