admin/features/events/eventEditPolicy.test.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

108 lines
3.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
EVENT_SENSITIVE_FIELDS,
isEventEnded,
isEventSlugLocked,
isFieldLocked,
resolveEventEditPolicy,
} from '@/features/events/eventEditPolicy'
describe('resolveEventEditPolicy', () => {
it('allows full edits for draft events', () => {
const policy = resolveEventEditPolicy('draft', 0)
expect(policy).toMatchObject({
canEdit: true,
hasActiveBookings: false,
minCapacity: 1,
lockBehavioralSettings: false,
})
expect(policy.lockedFields.size).toBe(0)
})
it('allows full edits for published events without bookings', () => {
const policy = resolveEventEditPolicy('published', 0)
expect(policy.canEdit).toBe(true)
expect(policy.hasActiveBookings).toBe(false)
expect(policy.minCapacity).toBe(1)
expect(policy.lockedFields.size).toBe(0)
expect(policy.lockBehavioralSettings).toBe(false)
})
it('does not lock the public slug on live statuses (revision path)', () => {
expect(isEventSlugLocked('draft')).toBe(false)
expect(isEventSlugLocked('pending_review')).toBe(false)
expect(isEventSlugLocked('published')).toBe(false)
expect(isEventSlugLocked('full')).toBe(false)
expect(isEventSlugLocked('completed')).toBe(true)
})
it('floors capacity at booked plus reserved seats', () => {
const policy = resolveEventEditPolicy('published', 2, 3)
expect(policy.minCapacity).toBe(5)
})
it('locks sensitive fields when a live event has active bookings', () => {
const policy = resolveEventEditPolicy('published', 3)
expect(policy).toMatchObject({
canEdit: true,
hasActiveBookings: true,
minCapacity: 3,
lockBehavioralSettings: true,
})
expect([...policy.lockedFields]).toEqual([...EVENT_SENSITIVE_FIELDS])
expect(isFieldLocked(policy, 'startsAt')).toBe(true)
expect(isFieldLocked(policy, 'price')).toBe(true)
expect(isFieldLocked(policy, 'cancellationFeePercent')).toBe(true)
expect(isFieldLocked(policy, 'genderRestriction')).toBe(true)
expect(isFieldLocked(policy, 'ageRestriction')).toBe(true)
})
it('locks the same fields for full events with bookings', () => {
const policy = resolveEventEditPolicy('full', 5)
expect(policy.minCapacity).toBe(5)
expect(policy.lockedFields.has('address')).toBe(true)
})
it('blocks edits for terminal statuses', () => {
for (const status of ['cancelled', 'completed']) {
const policy = resolveEventEditPolicy(status, 2)
expect(policy.canEdit).toBe(false)
expect(policy.minCapacity).toBe(2)
expect(policy.lockedFields.size).toBe(EVENT_SENSITIVE_FIELDS.length)
}
})
it('treats pending_review like draft for lock purposes', () => {
const policy = resolveEventEditPolicy('pending_review', 0)
expect(policy.canEdit).toBe(true)
expect(policy.hasActiveBookings).toBe(false)
expect(policy.lockedFields.size).toBe(0)
})
})
describe('isEventEnded', () => {
const future = new Date(Date.now() + 60 * 60 * 1000).toISOString()
const past = new Date(Date.now() - 60 * 60 * 1000).toISOString()
it('is true for terminal statuses even when endsAt is still in the future', () => {
expect(isEventEnded('completed', future)).toBe(true)
expect(isEventEnded('cancelled', future)).toBe(true)
})
it('is true when endsAt has passed on a live event', () => {
expect(isEventEnded('published', past)).toBe(true)
})
it('is false for a live event that has not ended yet', () => {
expect(isEventEnded('published', future)).toBe(false)
})
})