mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
534 lines
16 KiB
TypeScript
534 lines
16 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import type { Payload, PayloadRequest, RequiredDataFromCollectionSlug } from 'payload'
|
|
import { assert, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { assignMunicipalityAdvisorsRecord } from '@/app/(campaign)/campanha/actions/municipality'
|
|
import { CampaignUser as CampaignUserCollection } from '@/collections/CampaignUser'
|
|
import { CAMPAIGN_SESSION_TTL_LONG } from '@/lib/campaignSessionTtl'
|
|
import { campaignLoginSchema } from '@/lib/schemas/campaign-login'
|
|
import type { CampaignUser } from '@/payload-types'
|
|
import config from '@/payload.config'
|
|
import { getAccessibleMunicipalityIds, isCampaignCoordinator } from '@/utilities/campaignAccess'
|
|
import { authenticateCampaignToken, revokeCampaignSession } from '@/utilities/campaignAuth'
|
|
import { getPayload } from 'payload'
|
|
|
|
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
|
import { stub } from '../helpers/stub'
|
|
|
|
/** Create data with `role` intentionally omitted so the collection default applies. */
|
|
type CampaignUserCreateData = RequiredDataFromCollectionSlug<'campaignUser'>
|
|
|
|
let payload: Payload
|
|
const campaignFixtures = installCampaignFixtures({
|
|
getPayload: () => payload,
|
|
setPayload: (nextPayload) => {
|
|
payload = nextPayload
|
|
},
|
|
})
|
|
|
|
describe('campaign authentication foundation', () => {
|
|
beforeAll(async () => {
|
|
payload = await getPayload({ config: await config })
|
|
})
|
|
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('configures optional username and email login', () => {
|
|
expect(CampaignUserCollection.auth).toMatchObject({
|
|
tokenExpiration: CAMPAIGN_SESSION_TTL_LONG,
|
|
loginWithUsername: {
|
|
allowEmailLogin: true,
|
|
requireEmail: false,
|
|
requireUsername: false,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('defaults newly created campaign users to leader', async () => {
|
|
const username = campaignFixtures().phone()
|
|
const user = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: stub<CampaignUserCreateData>({
|
|
name: 'Liderança padrão',
|
|
username,
|
|
password: campaignFixtures().value('password'),
|
|
}),
|
|
})
|
|
|
|
expect(user.role).toBe('leader')
|
|
})
|
|
|
|
it('authenticates staff by email and leadership by normalized phone', async () => {
|
|
const password = campaignFixtures().value('password')
|
|
const email = `${campaignFixtures().value('staff')}@example.com`
|
|
const phone = campaignFixtures().phone()
|
|
|
|
await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Coordenação',
|
|
email,
|
|
password,
|
|
role: 'coordinator',
|
|
},
|
|
})
|
|
await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Liderança',
|
|
username: phone,
|
|
password,
|
|
role: 'leader',
|
|
},
|
|
})
|
|
|
|
const staffLogin = await payload.login({
|
|
collection: 'campaignUser',
|
|
data: { email, password },
|
|
})
|
|
const leadershipLogin = await payload.login({
|
|
collection: 'campaignUser',
|
|
data: { username: phone, password },
|
|
})
|
|
|
|
expect(staffLogin.user?.email).toBe(email)
|
|
expect(leadershipLogin.user?.username).toBe(phone)
|
|
})
|
|
|
|
it('stops authenticating a token after its server session is revoked', async () => {
|
|
const email = `${campaignFixtures().value('logout')}@example.com`
|
|
const password = campaignFixtures().value('password')
|
|
await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Sessão encerrada',
|
|
email,
|
|
password,
|
|
role: 'advisor',
|
|
},
|
|
})
|
|
const login = await payload.login({
|
|
collection: 'campaignUser',
|
|
data: { email, password },
|
|
})
|
|
assert(login.token)
|
|
const token = login.token
|
|
|
|
await expect(authenticateCampaignToken(token, payload)).resolves.not.toBeNull()
|
|
await revokeCampaignSession(token, payload)
|
|
await expect(authenticateCampaignToken(token, payload)).resolves.toBeNull()
|
|
})
|
|
|
|
it('preserves leading and trailing password spaces through validation and Payload login', async () => {
|
|
const password = ` ${campaignFixtures().value('exact-password')} `
|
|
const trimmedPassword = password.trim()
|
|
const email = `${campaignFixtures().value('spaced-password')}@example.com`
|
|
|
|
await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Senha com espaços',
|
|
email,
|
|
password,
|
|
role: 'coordinator',
|
|
},
|
|
})
|
|
|
|
const parsed = campaignLoginSchema.parse({
|
|
identifier: ` ${email} `,
|
|
password,
|
|
})
|
|
expect(parsed).toEqual({ identifier: email, password })
|
|
await expect(
|
|
payload.login({
|
|
collection: 'campaignUser',
|
|
data: { email: parsed.identifier, password: parsed.password },
|
|
}),
|
|
).resolves.toHaveProperty('token')
|
|
await expect(
|
|
payload.login({
|
|
collection: 'campaignUser',
|
|
data: { email, password: trimmedPassword },
|
|
}),
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it('re-reads the campaign user so a role downgrade applies immediately', async () => {
|
|
const staleJwtUser = {
|
|
id: 42,
|
|
collection: 'campaignUser',
|
|
role: 'coordinator',
|
|
} as CampaignUser
|
|
const currentUser = {
|
|
...staleJwtUser,
|
|
role: 'advisor',
|
|
} as CampaignUser
|
|
const auth = vi.fn().mockResolvedValue({ user: staleJwtUser })
|
|
const findByID = vi.fn().mockResolvedValue(currentUser)
|
|
|
|
const result = await authenticateCampaignToken('token', {
|
|
auth,
|
|
findByID,
|
|
})
|
|
|
|
expect(findByID).toHaveBeenCalledWith({
|
|
collection: 'campaignUser',
|
|
id: staleJwtUser.id,
|
|
depth: 0,
|
|
})
|
|
expect(result?.email).toBe('')
|
|
expect(result?.role).toBe('advisor')
|
|
expect(isCampaignCoordinator(result)).toBe(false)
|
|
})
|
|
|
|
it('denies management when a stale JWT still says coordinator after downgrade', async () => {
|
|
const staleCoordinator = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Coordenação rebaixada',
|
|
email: `${campaignFixtures().value('downgraded')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
role: 'coordinator',
|
|
},
|
|
})
|
|
|
|
await payload.update({
|
|
collection: 'campaignUser',
|
|
id: staleCoordinator.id,
|
|
data: {
|
|
role: 'advisor',
|
|
},
|
|
})
|
|
|
|
await expect(
|
|
payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Criação indevida',
|
|
email: `${campaignFixtures().value('denied')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
role: 'coordinator',
|
|
},
|
|
user: staleCoordinator,
|
|
overrideAccess: false,
|
|
}),
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it.each(['coordinator', 'advisor', 'leader'] as const)(
|
|
'hides identity and private auth fields from another %s user',
|
|
async (role) => {
|
|
const viewer = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: `Viewer ${role}`,
|
|
email: `${campaignFixtures().value(role)}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
role,
|
|
},
|
|
})
|
|
const target = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Outra liderança',
|
|
username: campaignFixtures().phone(),
|
|
password: campaignFixtures().value('password'),
|
|
role: 'leader',
|
|
},
|
|
})
|
|
|
|
const visibleTarget = await payload.findByID({
|
|
collection: 'campaignUser',
|
|
id: target.id,
|
|
user: viewer,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
expect(visibleTarget.name).toBe(target.name)
|
|
expect(visibleTarget.email).toBeUndefined()
|
|
expect(visibleTarget.username).toBeUndefined()
|
|
expect(visibleTarget.hash).toBeUndefined()
|
|
expect(visibleTarget.salt).toBeUndefined()
|
|
expect(visibleTarget.sessions).toBeUndefined()
|
|
expect(visibleTarget.loginAttempts).toBeUndefined()
|
|
expect(visibleTarget.lockUntil).toBeUndefined()
|
|
expect(visibleTarget.resetPasswordToken).toBeUndefined()
|
|
expect(visibleTarget.resetPasswordExpiration).toBeUndefined()
|
|
},
|
|
)
|
|
|
|
it('allows a campaign user to read their own identity fields', async () => {
|
|
const username = campaignFixtures().phone()
|
|
const target = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Liderança titular',
|
|
username,
|
|
password: campaignFixtures().value('password'),
|
|
role: 'leader',
|
|
},
|
|
})
|
|
|
|
const visibleTarget = await payload.findByID({
|
|
collection: 'campaignUser',
|
|
id: target.id,
|
|
user: target,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
expect(visibleTarget.username).toBe(username)
|
|
})
|
|
|
|
it('allows a Payload admin to read campaign user identity fields', async () => {
|
|
const admin = await campaignFixtures().createAdminUser({
|
|
email: `${campaignFixtures().value('admin')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
})
|
|
const username = campaignFixtures().phone()
|
|
const target = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: stub<CampaignUserCreateData>({
|
|
name: 'Liderança administrada',
|
|
username,
|
|
password: campaignFixtures().value('password'),
|
|
}),
|
|
})
|
|
|
|
const visibleTarget = await payload.findByID({
|
|
collection: 'campaignUser',
|
|
id: target.id,
|
|
user: admin,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
expect(visibleTarget.username).toBe(username)
|
|
})
|
|
|
|
it.each([
|
|
{ actorKind: 'admin', targetRole: 'advisor' },
|
|
{ actorKind: 'coordinator', targetRole: 'coordinator' },
|
|
] as const)(
|
|
'blocks an assigned $targetRole downgrade from a Payload $actorKind while advisor of a municipality',
|
|
async ({ actorKind, targetRole }) => {
|
|
const coordinator = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Coordenação responsável',
|
|
email: `${campaignFixtures().value('coordinator')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
role: 'coordinator',
|
|
},
|
|
})
|
|
const admin =
|
|
actorKind === 'admin'
|
|
? await campaignFixtures().createAdminUser({
|
|
email: `${campaignFixtures().value('admin')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
})
|
|
: null
|
|
const target = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Responsável ainda designado',
|
|
email: `${campaignFixtures().value('assigned')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
role: targetRole,
|
|
},
|
|
})
|
|
const municipality = await campaignFixtures().getMunicipality()
|
|
await campaignFixtures().assignMunicipalityAdvisors(municipality, [target])
|
|
const actor = admin ?? coordinator
|
|
|
|
await expect(
|
|
payload.update({
|
|
collection: 'campaignUser',
|
|
id: target.id,
|
|
data: { role: 'leader' },
|
|
user: actor,
|
|
overrideAccess: false,
|
|
}),
|
|
).rejects.toThrow(
|
|
'Remova ou substitua este usuário da assessoria de municípios, dobradinhas ou lideranças antes de alterar o papel para liderança.',
|
|
)
|
|
|
|
if (admin) {
|
|
await payload.update({
|
|
collection: 'municipality',
|
|
id: municipality.id,
|
|
data: { advisors: [] },
|
|
user: admin,
|
|
overrideAccess: false,
|
|
depth: 0,
|
|
})
|
|
} else {
|
|
await assignMunicipalityAdvisorsRecord(payload, coordinator, {
|
|
municipality: municipality.id,
|
|
advisors: [],
|
|
})
|
|
}
|
|
const downgraded = await payload.update({
|
|
collection: 'campaignUser',
|
|
id: target.id,
|
|
data: { role: 'leader' },
|
|
user: actor,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
expect(downgraded.role).toBe('leader')
|
|
},
|
|
)
|
|
|
|
it.each(['advisor', 'leader'] as const)(
|
|
'denies campaignUser CRUD management to an ordinary %s',
|
|
async (role) => {
|
|
const actor = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: `Actor ${role}`,
|
|
email: `${campaignFixtures().value(role)}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
role,
|
|
},
|
|
})
|
|
const target = await payload.create({
|
|
collection: 'campaignUser',
|
|
data: {
|
|
name: 'Alvo',
|
|
email: `${campaignFixtures().value('target')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
role: 'leader',
|
|
},
|
|
})
|
|
|
|
await expect(
|
|
payload.create({
|
|
collection: 'campaignUser',
|
|
data: stub<CampaignUserCreateData>({
|
|
name: 'Criação indevida',
|
|
email: `${campaignFixtures().value('denied')}@example.com`,
|
|
password: campaignFixtures().value('password'),
|
|
}),
|
|
user: actor,
|
|
overrideAccess: false,
|
|
}),
|
|
).rejects.toThrow()
|
|
await expect(
|
|
payload.update({
|
|
collection: 'campaignUser',
|
|
id: target.id,
|
|
data: { name: 'Atualização indevida' },
|
|
user: actor,
|
|
overrideAccess: false,
|
|
}),
|
|
).rejects.toThrow()
|
|
await expect(
|
|
payload.delete({
|
|
collection: 'campaignUser',
|
|
id: target.id,
|
|
user: actor,
|
|
overrideAccess: false,
|
|
}),
|
|
).rejects.toThrow()
|
|
},
|
|
)
|
|
|
|
it('returns only municipalities administered by an advisor', async () => {
|
|
const advisor = await campaignFixtures().createCampaignUser('advisor')
|
|
const assignedMunicipality = await campaignFixtures().getMunicipality()
|
|
const otherMunicipality = await campaignFixtures().getMunicipality()
|
|
await campaignFixtures().assignMunicipalityAdvisors(assignedMunicipality, [advisor])
|
|
|
|
const req = stub<PayloadRequest>({ context: {}, payload })
|
|
|
|
const ids = await getAccessibleMunicipalityIds(req, advisor)
|
|
|
|
expect(ids).toEqual([assignedMunicipality.id])
|
|
expect(ids).not.toContain(otherMunicipality.id)
|
|
})
|
|
|
|
it('uses the current role when resolving accessible municipalities', async () => {
|
|
const find = vi.fn().mockResolvedValue({
|
|
docs: [{ id: 7 }],
|
|
})
|
|
const staleCoordinator = {
|
|
id: 11,
|
|
collection: 'campaignUser',
|
|
role: 'coordinator',
|
|
} as CampaignUser
|
|
const currentAdvisor = {
|
|
...staleCoordinator,
|
|
role: 'advisor',
|
|
} as CampaignUser
|
|
const req = {
|
|
context: {},
|
|
payload: {
|
|
collections: {
|
|
municipality: {},
|
|
},
|
|
find,
|
|
findByID: vi.fn().mockResolvedValue(currentAdvisor),
|
|
},
|
|
user: staleCoordinator,
|
|
} as unknown as PayloadRequest
|
|
|
|
const ids = await getAccessibleMunicipalityIds(req)
|
|
|
|
expect(ids).toEqual([7])
|
|
expect(req.payload.findByID).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
collection: 'campaignUser',
|
|
id: staleCoordinator.id,
|
|
overrideAccess: true,
|
|
req,
|
|
}),
|
|
)
|
|
expect(find).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
collection: 'municipality',
|
|
req,
|
|
where: {
|
|
advisors: {
|
|
contains: staleCoordinator.id,
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('returns only engaged municipalities for a leader and propagates req', async () => {
|
|
const find = vi.fn().mockResolvedValue({
|
|
docs: [{ id: 3, municipalities: [7], organizations: [] }],
|
|
})
|
|
const req = {
|
|
context: {},
|
|
payload: {
|
|
collections: {
|
|
leadership: {},
|
|
},
|
|
find,
|
|
},
|
|
} as unknown as PayloadRequest
|
|
const user = {
|
|
id: 12,
|
|
collection: 'campaignUser',
|
|
role: 'leader',
|
|
} as CampaignUser
|
|
|
|
const ids = await getAccessibleMunicipalityIds(req, user)
|
|
|
|
expect(ids).toEqual([7])
|
|
expect(find).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
collection: 'leadership',
|
|
overrideAccess: true,
|
|
req,
|
|
where: {
|
|
and: [{ user: { equals: user.id } }, { supportStatus: { equals: 'engajado' } }],
|
|
},
|
|
}),
|
|
)
|
|
})
|
|
})
|