← Public packages
@kentcdodds/cal-com
Cal.com helpers for booking pages, event types, slots, and webhooks.
test/helpers.test.js
337 lines · 11.9 KB · JavaScriptimport assert from 'node:assert/strict'
import test from 'node:test'
import {
applyEventTypeIdToSlotsQuery,
canonicalizeWebhookTrigger,
eventTypeSelectorFromInput,
firstCatalogEventType,
flattenEventTypes,
formatCalErrorDetail,
formatEventTypeCatalog,
hasEventTypeLookupSelector,
hasEventTypeSelector,
isNumericEventTypeId,
isSmokeInput,
normalizeCreateBookingBody,
normalizeCreateWebhookBody,
planAvailableSlotsQuery,
resolveEventTypeFromCatalog,
utcDateOnly,
WEBHOOK_TRIGGERS,
} from '../src/helpers.js'
test('formatCalErrorDetail prefers the Cal.com error message', () => {
const detail = formatCalErrorDetail({
status: 'error',
error: {
code: 'BadRequestException',
message: 'attendee property is wrong, timeZone must be a valid IANA time-zone',
},
})
assert.equal(detail, 'attendee property is wrong, timeZone must be a valid IANA time-zone')
})
test('formatCalErrorDetail flattens webhook constraint errors', () => {
const detail = formatCalErrorDetail({
error: {
message: 'Bad Request Exception',
details: {
errors: [
{ property: 'active', constraints: { isBoolean: 'active must be a boolean value' } },
{ property: 'subscriberUrl', constraints: { isString: 'subscriberUrl must be a string' } },
],
},
},
})
assert.equal(detail, 'active must be a boolean value; subscriberUrl must be a string')
})
test('normalizeCreateBookingBody defaults attendee.timeZone to UTC', () => {
const body = normalizeCreateBookingBody({
eventTypeId: 123,
start: '2026-06-01T16:00:00Z',
attendee: { name: 'Ada', email: 'ada@example.com' },
})
assert.equal(body.attendee.timeZone, 'UTC')
assert.equal(body.eventTypeId, 123)
})
test('normalizeCreateBookingBody lifts top-level timeZone onto attendee', () => {
const body = normalizeCreateBookingBody({
eventTypeId: 123,
start: '2026-06-01T16:00:00Z',
timeZone: 'America/Denver',
attendee: { name: 'Ada', email: 'ada@example.com' },
})
assert.equal(body.attendee.timeZone, 'America/Denver')
assert.equal('timeZone' in body, false)
})
test('normalizeCreateBookingBody rejects missing required fields', () => {
assert.throws(() => normalizeCreateBookingBody({}), /attendee/)
assert.throws(
() =>
normalizeCreateBookingBody({
start: '2026-06-01T16:00:00Z',
attendee: { name: 'Ada', email: 'ada@example.com' },
}),
/eventTypeId/,
)
})
test('normalizeCreateWebhookBody defaults active to true', () => {
const body = normalizeCreateWebhookBody({
subscriberUrl: 'https://example.com/hooks/cal',
triggers: ['BOOKING_CREATED'],
})
assert.equal(body.active, true)
assert.deepEqual(body.triggers, ['BOOKING_CREATED'])
})
test('normalizeCreateWebhookBody rejects missing subscriberUrl and triggers', () => {
assert.throws(() => normalizeCreateWebhookBody({}), /subscriberUrl/)
assert.throws(
() => normalizeCreateWebhookBody({ subscriberUrl: 'https://example.com/hooks/cal' }),
/triggers/,
)
})
test('normalizeCreateWebhookBody canonicalizes trigger aliases and accepts a string', () => {
const body = normalizeCreateWebhookBody({
subscriberUrl: 'https://example.com/hooks/cal',
triggers: ['booking-created', 'BOOKING.CANCELLED', 'booking created', 'BOOKING_CREATED'],
})
assert.deepEqual(body.triggers, ['BOOKING_CREATED', 'BOOKING_CANCELLED'])
const fromString = normalizeCreateWebhookBody({
subscriberUrl: 'https://example.com/hooks/cal',
triggers: 'meeting-started',
})
assert.deepEqual(fromString.triggers, ['MEETING_STARTED'])
})
test('normalizeCreateWebhookBody rejects unofficial trigger names locally', () => {
assert.throws(
() =>
normalizeCreateWebhookBody({
subscriberUrl: 'https://example.com/hooks/cal',
triggers: ['BOOKING_CONFIRMED'],
}),
(error) => {
assert.match(error.message, /BOOKING_CONFIRMED/)
assert.match(error.message, /Allowed:[\s\S]*BOOKING_CREATED/)
assert.doesNotMatch(error.message, /Cal\.com API request failed/)
return true
},
)
})
test('canonicalizeWebhookTrigger maps kebab, dotted, and spaced names', () => {
assert.equal(canonicalizeWebhookTrigger('booking-created'), 'BOOKING_CREATED')
assert.equal(canonicalizeWebhookTrigger('booking.created'), 'BOOKING_CREATED')
assert.equal(canonicalizeWebhookTrigger(' booking created '), 'BOOKING_CREATED')
assert.ok(WEBHOOK_TRIGGERS.includes('RECORDING_TRANSCRIPTION_GENERATED'))
assert.equal(WEBHOOK_TRIGGERS.includes('BOOKING_CONFIRMED'), false)
})
test('flattenEventTypes accepts arrays, eventTypes, and eventTypeGroups', () => {
const row = { id: 101, title: '30 Min Meeting', slug: '30mins' }
assert.deepEqual(flattenEventTypes([row]), [row])
assert.deepEqual(flattenEventTypes({ eventTypes: [row] }), [row])
assert.deepEqual(flattenEventTypes({ eventTypeGroups: [{ eventTypes: [row] }] }), [row])
assert.deepEqual(flattenEventTypes({}), [])
})
test('formatEventTypeCatalog lists ids, titles, and slugs', () => {
const catalog = formatEventTypeCatalog(
[
{ id: 101, title: '30 Min Meeting', slug: '30mins' },
{ id: 102, name: '60 Min Meeting', slug: '60mins' },
],
{ limit: 8 },
)
assert.match(catalog, /101 "30 Min Meeting" \(30mins\)/)
assert.match(catalog, /102 "60 Min Meeting" \(60mins\)/)
assert.match(catalog, /list-event-types/)
})
test('formatEventTypeCatalog truncates and reports leftovers', () => {
const types = Array.from({ length: 10 }, (_, index) => ({
id: index + 1,
title: `Type ${index + 1}`,
}))
const catalog = formatEventTypeCatalog(types, { limit: 3 })
assert.match(catalog, /1 "Type 1"/)
assert.match(catalog, /\+7 more/)
assert.equal(catalog.includes('Type 4'), false)
})
test('formatEventTypeCatalog explains an empty account', () => {
assert.match(formatEventTypeCatalog([]), /No event types found/)
})
test('hasEventTypeSelector accepts id or slug plus username/teamSlug', () => {
assert.equal(hasEventTypeSelector({ eventTypeId: 101 }), true)
assert.equal(hasEventTypeSelector({ eventTypeSlug: '30mins', username: 'kent' }), true)
assert.equal(hasEventTypeSelector({ eventTypeSlug: '30mins', teamSlug: 'team' }), true)
assert.equal(hasEventTypeSelector({ eventTypeSlug: '30mins' }), false)
assert.equal(hasEventTypeSelector({}), false)
})
test('isNumericEventTypeId accepts positive integer ids', () => {
assert.equal(isNumericEventTypeId(587527), true)
assert.equal(isNumericEventTypeId('587527'), true)
assert.equal(isNumericEventTypeId(' 587527 '), true)
assert.equal(isNumericEventTypeId('30mins'), false)
assert.equal(isNumericEventTypeId('30 Min Meeting'), false)
assert.equal(isNumericEventTypeId(0), false)
assert.equal(isNumericEventTypeId(-1), false)
})
test('eventTypeSelectorFromInput reads id aliases and params.id', () => {
assert.deepEqual(eventTypeSelectorFromInput({ id: 101 }), {
id: 101,
slug: '',
title: '',
})
assert.deepEqual(eventTypeSelectorFromInput({ eventTypeId: '102' }), {
id: '102',
slug: '',
title: '',
})
assert.deepEqual(eventTypeSelectorFromInput({ params: { id: 103 } }), {
id: 103,
slug: '',
title: '',
})
})
test('eventTypeSelectorFromInput treats non-numeric id as slug or title', () => {
assert.deepEqual(eventTypeSelectorFromInput({ id: '30mins' }), {
id: undefined,
slug: '30mins',
title: '30mins',
})
assert.deepEqual(eventTypeSelectorFromInput({ eventTypeSlug: '30mins' }), {
id: undefined,
slug: '30mins',
title: '',
})
assert.deepEqual(eventTypeSelectorFromInput({ title: '30 Min Meeting' }), {
id: undefined,
slug: '',
title: '30 Min Meeting',
})
assert.equal(hasEventTypeLookupSelector(eventTypeSelectorFromInput({})), false)
assert.equal(hasEventTypeLookupSelector(eventTypeSelectorFromInput({ slug: '30mins' })), true)
})
test('resolveEventTypeFromCatalog matches id, slug, or unique title', () => {
const types = [
{ id: 101, title: '30 Min Meeting', slug: '30mins' },
{ id: 102, name: '60 Min Meeting', slug: '60mins' },
]
assert.equal(resolveEventTypeFromCatalog(types, { id: 101 })?.id, 101)
assert.equal(resolveEventTypeFromCatalog(types, { slug: '60MINS' })?.id, 102)
assert.equal(resolveEventTypeFromCatalog(types, { title: '30 min meeting' })?.id, 101)
assert.equal(resolveEventTypeFromCatalog(types, { slug: 'missing' }), null)
assert.equal(resolveEventTypeFromCatalog(types, {}), null)
})
test('resolveEventTypeFromCatalog ignores ambiguous titles', () => {
const types = [
{ id: 1, title: 'Meet', slug: 'meet-a' },
{ id: 2, title: 'Meet', slug: 'meet-b' },
]
assert.equal(resolveEventTypeFromCatalog(types, { title: 'Meet' }), null)
assert.equal(resolveEventTypeFromCatalog(types, { slug: 'meet-b' })?.id, 2)
})
test('planAvailableSlotsQuery lists when no event-type selector is given', () => {
assert.deepEqual(planAvailableSlotsQuery({}), { kind: 'list' })
assert.deepEqual(
planAvailableSlotsQuery({ start: '2026-06-01', end: '2026-06-08' }),
{ kind: 'list' },
)
})
test('planAvailableSlotsQuery keeps official Cal.com slots selectors', () => {
assert.deepEqual(planAvailableSlotsQuery({ eventTypeId: 101, start: '2026-06-01' }), {
kind: 'ready',
query: { eventTypeId: 101, start: '2026-06-01' },
})
assert.deepEqual(
planAvailableSlotsQuery({ eventTypeSlug: '30mins', username: 'kent', start: '2026-06-01' }),
{
kind: 'ready',
query: { eventTypeSlug: '30mins', username: 'kent', start: '2026-06-01' },
},
)
})
test('planAvailableSlotsQuery maps numeric id aliases onto eventTypeId', () => {
assert.deepEqual(planAvailableSlotsQuery({ id: 101, start: '2026-06-01' }), {
kind: 'ready',
query: { eventTypeId: 101, start: '2026-06-01' },
})
assert.deepEqual(planAvailableSlotsQuery({ params: { eventTypeId: '102' }, end: '2026-06-08' }), {
kind: 'ready',
query: { eventTypeId: '102', end: '2026-06-08' },
})
})
test('planAvailableSlotsQuery resolves slug or title aliases via the catalog', () => {
assert.deepEqual(planAvailableSlotsQuery({ slug: '30mins', start: '2026-06-01' }), {
kind: 'resolve',
query: { slug: '30mins', start: '2026-06-01' },
selector: { id: undefined, slug: '30mins', title: '' },
})
assert.deepEqual(planAvailableSlotsQuery({ eventTypeSlug: '30mins', start: '2026-06-01' }), {
kind: 'resolve',
query: { eventTypeSlug: '30mins', start: '2026-06-01' },
selector: { id: undefined, slug: '30mins', title: '' },
})
assert.deepEqual(planAvailableSlotsQuery({ title: '30 Min Meeting' }), {
kind: 'resolve',
query: { title: '30 Min Meeting' },
selector: { id: undefined, slug: '', title: '30 Min Meeting' },
})
})
test('isSmokeInput is only true for smoke: true', () => {
assert.equal(isSmokeInput({ smoke: true }), true)
assert.equal(isSmokeInput({ smoke: true, eventTypeId: 999999999 }), true)
assert.equal(isSmokeInput({ smoke: false }), false)
assert.equal(isSmokeInput({ eventTypeId: 101 }), false)
assert.equal(isSmokeInput({}), false)
})
test('firstCatalogEventType picks the first row with an id', () => {
assert.equal(
firstCatalogEventType([
{ title: 'no id' },
{ id: 101, title: '30 Min Meeting' },
{ id: 102, title: '60 Min Meeting' },
])?.id,
101,
)
assert.equal(firstCatalogEventType([]), null)
assert.equal(firstCatalogEventType({ eventTypes: [{ id: 587527, slug: '30mins' }] })?.id, 587527)
})
test('utcDateOnly offsets from a fixed instant', () => {
const now = new Date('2026-08-23T04:00:00.000Z')
assert.equal(utcDateOnly(0, now), '2026-08-23')
assert.equal(utcDateOnly(7, now), '2026-08-30')
})
test('planAvailableSlotsQuery treats smoke-only input as list', () => {
assert.deepEqual(planAvailableSlotsQuery({ smoke: true }), { kind: 'list' })
})
test('applyEventTypeIdToSlotsQuery strips id/slug/title aliases', () => {
assert.deepEqual(
applyEventTypeIdToSlotsQuery({ slug: '30mins', title: '30 Min Meeting', start: '2026-06-01' }, 101),
{ eventTypeId: 101, start: '2026-06-01' },
)
})