conferencing.test.ts8.4 KBView on GitHub import {
canCreateZoom,
conferenceCreateRequest,
conferenceErrorMessage,
conferenceOnEvent,
conferenceOptionsFor,
minutesBetween,
type ZoomStatus,
} from '@/modules/calendar/utils/conferencing';
/**
* What Cedar offers when you ask to put a video call on an event.
*
* This used to be a question about the CALENDAR: Meet always, plus an "add-on" option gated on
* `conferenceProperties.allowedConferenceSolutionTypes` containing `addOn`. Probed live, every
* calendar on every account reports exactly `["hangoutsMeet"]` — that field says what an API
* client may create, and Google only lets one create its own solutions. The add-on option was
* unreachable code, and had it rendered, the request would have failed.
*
* It is now a question about the USER: has this person connected Zoom. Cedar mints the meeting on
* their Zoom account and writes the join details onto the event.
* See apps/server/docs/zoom-conferencing.md.
*/
const status = (overrides: Partial<ZoomStatus> = {}): ZoomStatus => ({
configured: true,
connected: true,
needsReauth: false,
...overrides,
});
describe('conferenceOptionsFor', () => {
it('offers Zoom to a user who has connected it', () => {
const options = conferenceOptionsFor(status());
expect(options.map((o) => o.key)).toEqual(['hangoutsMeet', 'zoom']);
expect(options[1]!.label).toBe('Zoom');
expect(options[1]!.requiresConnect).toBeFalsy();
});
it('offers to CONNECT Zoom rather than hiding it, when the user has not', () => {
// A disabled row does not tell anyone what to do about it; this one does, and clicking it
// fixes the thing it is complaining about.
const options = conferenceOptionsFor(status({ connected: false }));
expect(options[1]).toMatchObject({ key=[redacted], label: 'Connect Zoom', requiresConnect: true });
});
it('says RE-connect when a connection existed and its token died', () => {
// "Connect Zoom" to someone who connected Zoom last week reads as though Cedar lost their
// setup. The word is the whole difference between a bug report and a click.
const options = conferenceOptionsFor(status({ connected: false, needsReauth: true }));
expect(options[1]).toMatchObject({ key=[redacted], label: 'Reconnect Zoom', requiresConnect: true });
});
it('offers Zoom even where the server has no Zoom app', () => {
// Zoom is a product capability, not a per-deployment one. Hiding it behind `ZOOM_CLIENT_ID`
// made the feature invisible to everyone until an env var landed somewhere, and left nobody
// able to tell whether it was missing or broken. The honest failure for an unconfigured
// server is a message inside the connect dialog, not an absence in the picker.
const options = conferenceOptionsFor(status({ configured: false, connected: false }));
expect(options.map((o) => o.key)).toEqual(['hangoutsMeet', 'zoom']);
expect(options[1]!.requiresConnect).toBe(true);
});
it('offers both while the status is still loading', () => {
// The picker renders before the query resolves. An unknown status reads as not-connected, so
// the option is there from the first frame and only its WORDING settles — rather than the
// control popping into existence a beat later.
for (const unknown of [null, undefined]) {
const options = conferenceOptionsFor(unknown);
expect(options.map((o) => o.key)).toEqual(['hangoutsMeet', 'zoom']);
expect(options[1]!.label).toBe('Connect Zoom');
}
});
});
describe('canCreateZoom', () => {
it('turns on `connected`, and nothing else', () => {
// `configured` is deliberately not consulted: a user cannot be connected to an app that does
// not exist, so checking it could only ever hide the option from someone able to use it.
expect(canCreateZoom(status())).toBe(true);
expect(canCreateZoom(status({ connected: false }))).toBe(false);
expect(canCreateZoom(null)).toBe(false);
});
});
describe('conferenceOnEvent', () => {
it("uses Google's own name, so a Zoom meeting does not read Google Meet", () => {
const event = {
conferenceData: {
conferenceSolution: { name: 'Zoom Meeting' },
entryPoints: [{ entryPointType: 'video', uri: 'https://us02web.zoom.us/j/123' }],
},
};
expect(conferenceOnEvent(event)).toEqual({
name: 'Zoom Meeting',
uri: 'https://us02web.zoom.us/j/123',
});
});
it('names an unlabelled conference generically rather than wrongly', () => {
const event = {
conferenceData: { entryPoints: [{ entryPointType: 'video', uri: 'https://example.com/x' }] },
};
expect(conferenceOnEvent(event)?.name).toBe('Video call');
});
it('falls back to the location, which is where Cedar writes a Zoom link', () => {
// The load-bearing case: `conferenceData` is best-effort — Google may decline to store entry
// points from an outside client — while `location` always sticks. Without this fallback the
// panel would claim there is no call on an event whose invite plainly shows one.
expect(conferenceOnEvent({ location: 'https://us02web.zoom.us/j/12345678901' })).toEqual({
name: 'Zoom Meeting',
uri: 'https://us02web.zoom.us/j/12345678901',
});
expect(conferenceOnEvent({ location: 'https://meet.google.com/abc-defg-hij' })?.name).toBe(
'Google Meet',
);
});
it('does not mistake a street address for a meeting link', () => {
expect(conferenceOnEvent({ location: '600 Congress Ave, Austin' })).toBeNull();
expect(conferenceOnEvent({ location: 'https://notion.so/some-doc' })).toBeNull();
});
it('reports nothing when the event has neither', () => {
expect(conferenceOnEvent({})).toBeNull();
expect(conferenceOnEvent(null)).toBeNull();
});
});
describe('conferenceCreateRequest', () => {
it('asks only for Meet — the one solution Google will create for a client', () => {
const { conferenceData } = conferenceCreateRequest('req-1');
expect(conferenceData.createRequest.conferenceSolutionKey.type).toBe('hangoutsMeet');
expect(conferenceData.createRequest.requestId).toBe('req-1');
});
it('always carries the version — without it Google ignores the block', () => {
expect(conferenceCreateRequest('req-2').conferenceDataVersion).toBe(1);
});
});
describe('conferenceErrorMessage', () => {
it("passes a Zoom failure through, because the server already wrote it for a human", () => {
// `zoom-meetings.ts` is the only layer that knows what Zoom's code 124 means; flattening its
// translation here would throw away the only actionable sentence in the stack.
const message = conferenceErrorMessage(
'zoom',
new Error('Your Zoom connection expired. Reconnect Zoom and try again.'),
);
expect(message).toBe('Your Zoom connection expired. Reconnect Zoom and try again.');
});
it('does not show a raw exception name to the user', () => {
expect(conferenceErrorMessage('zoom', new Error('TypeError: fetch failed'))).toBe(
'Could not create the Zoom meeting',
);
expect(conferenceErrorMessage('zoom', undefined)).toBe('Could not create the Zoom meeting');
});
it('leaves an ordinary Meet failure ordinary', () => {
expect(conferenceErrorMessage('hangoutsMeet', new Error('boom'))).toBe(
'Failed to add Google Meet',
);
});
});
describe('minutesBetween', () => {
it('is the real length, not Zoom’s 60-minute default', () => {
// Zoom fills in 60 when no duration is sent, so a 25-minute call would be booked as an hour
// on the host's account — visible to them, and wrong on any Zoom-side report.
expect(minutesBetween('2026-09-02T15:00:00Z', '2026-09-02T15:25:00Z')).toBe(25);
expect(minutesBetween('2026-09-02T15:00:00Z', '2026-09-02T16:30:00Z')).toBe(90);
});
it('rounds up, so a meeting is never cut short', () => {
expect(minutesBetween('2026-09-02T15:00:00Z', '2026-09-02T15:25:30Z')).toBe(26);
});
it('declines to guess rather than sending a wrong number', () => {
expect(minutesBetween(undefined, '2026-09-02T15:25:00Z')).toBeUndefined();
expect(minutesBetween('2026-09-02T15:00:00Z', null)).toBeUndefined();
// An inverted range is a bug upstream; passing a negative duration on would make Zoom the
// place it surfaces, which is the hardest place to debug it from.
expect(minutesBetween('2026-09-02T16:00:00Z', '2026-09-02T15:00:00Z')).toBeUndefined();
expect(minutesBetween('not a date', '2026-09-02T15:00:00Z')).toBeUndefined();
});
});