Files
calendar/packages/features/calendar-subscription/lib/cache/__tests__/CalendarCacheEventService.test.ts
T
Volnei MunhozGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Alex van AndelKeith Williams
e6b2116a2b feat: Calendar Cache and Sync (#23876)
* feat: calendar cache and sync - wip

* Add env.example

* refactor on CalendarCacheEventService

* remove test console.log

* Fix type checks errors

* chore: remove pt comment

* add route.ts

* chore: fix tests

* Improve cache impl

* chore: update recurring event id

* chore: small improvements

* calendar cache improvements

* Fix remove dynamic imports

* Add cleanup stale cache

* Fix tests

* add event update

* type fixes

* feat: add comprehensive tests for new calendar subscription API routes

- Add tests for /api/cron/calendar-subscriptions-cleanup route (9 tests)
- Add tests for /api/cron/calendar-subscriptions route (10 tests)
- Add tests for /api/webhooks/calendar-subscription/[provider] route (11 tests)
- Add missing feature flags for calendar-subscription-cache and calendar-subscription-sync
- All 30 tests pass with comprehensive coverage of authentication, feature flags, error handling, and service instantiation

Tests cover:
- Authentication scenarios (API key validation, Bearer tokens, query parameters)
- Feature flag combinations (cache/sync enabled/disabled states)
- Success and error handling (including non-Error exceptions)
- Service instantiation with proper dependency injection
- Provider validation for webhook endpoints

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* feat: add comprehensive tests for calendar subscription services, repositories, and adapters

- Add unit tests for CalendarSubscriptionService with subscription, webhook, and event processing
- Add unit tests for CalendarCacheEventService with cache operations and cleanup
- Add unit tests for CalendarSyncService with Cal.com event filtering and booking operations
- Add unit tests for CalendarCacheEventRepository with CRUD operations
- Add unit tests for SelectedCalendarRepository with calendar selection management
- Add unit tests for GoogleCalendarSubscriptionAdapter with subscription and event fetching
- Add unit tests for Office365CalendarSubscriptionAdapter with placeholder implementation
- Add unit tests for AdaptersFactory with provider management and adapter creation
- Fix lint issues by removing explicit 'any' type casting and unused variables
- All tests follow Cal.com conventions using Vitest framework with proper mocking

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: improve calendar-subscriptions-cleanup test performance by adding missing mocks

- Add comprehensive mocks for defaultResponderForAppDir, logger, performance monitoring, and Sentry
- Fix slow test execution (933ms -> <100ms) caused by missing dependency mocks
- Ensure consistent test performance across different environments

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Fix tests

* Fix tests

* type fix

* Fix coderabbit comments

* Fix types

* Fix test

* Update apps/web/app/api/cron/calendar-subscriptions/route.ts

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Fixes by first review

* feat: add database migrations for calendar cache and sync fields

- Add CalendarCacheEventStatus enum with confirmed, tentative, cancelled values
- Add new fields to SelectedCalendar: channelId, channelKind, channelResourceId, channelResourceUri, channelExpiration, syncSubscribedAt, syncToken, syncedAt, syncErrorAt, syncErrorCount
- Create CalendarCacheEvent table with foreign key to SelectedCalendar
- Add necessary indexes and constraints for performance and data integrity

Fixes database schema issues causing e2e test failures with 'column does not exist' errors.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* only google-calendar for now

* docs: add Calendar Cache and Sync feature documentation

- Add comprehensive feature overview and motivation
- Document feature flags with SQL examples
- Include SQL examples for enabling features for users and teams
- Reference technical documentation files

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* docs: update calendar subscription README with comprehensive documentation

- Undo incorrect changes to main README.md
- Update packages/features/calendar-subscription/README.md with:
  - Feature overview and motivation
  - Environment variables section
  - Complete feature flags documentation with SQL examples
  - SQL examples for enabling features for users and teams
  - Detailed architecture documentation

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix docs

* Fix test to available calendars

* Fix test to available calendars

* add migration and sync boilerplate

* fix typo

* remove double log

* sync boilerplate

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-09-29 14:26:14 +00:00

355 lines
12 KiB
TypeScript

import { describe, test, expect, vi, beforeEach } from "vitest";
import type { CalendarSubscriptionEventItem } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionPort.interface";
import type { ICalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository.interface";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { CalendarCacheEventService } from "../CalendarCacheEventService";
const mockSelectedCalendar: SelectedCalendar = {
id: "test-calendar-id",
userId: 1,
credentialId: 1,
integration: "google_calendar",
externalId: "test@example.com",
eventTypeId: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
error: null,
lastErrorAt: null,
watchAttempts: 0,
maxAttempts: 3,
unwatchAttempts: 0,
createdAt: new Date(),
updatedAt: new Date(),
channelId: "test-channel-id",
channelKind: "web_hook",
channelResourceId: "test-resource-id",
channelResourceUri: "test-resource-uri",
channelExpiration: new Date(Date.now() + 86400000),
syncSubscribedAt: new Date(),
syncToken: "test-sync-token",
syncedAt: new Date(),
syncErrorAt: null,
syncErrorCount: 0,
};
describe("CalendarCacheEventService", () => {
let service: CalendarCacheEventService;
let mockRepository: ICalendarCacheEventRepository;
beforeEach(() => {
mockRepository = {
upsertMany: vi.fn().mockResolvedValue(undefined),
deleteMany: vi.fn().mockResolvedValue(undefined),
deleteAllBySelectedCalendarId: vi.fn().mockResolvedValue(undefined),
deleteStale: vi.fn().mockResolvedValue(undefined),
findAllBySelectedCalendarIdsBetween: vi.fn().mockResolvedValue([]),
};
service = new CalendarCacheEventService({
calendarCacheEventRepository: mockRepository,
});
});
describe("handleEvents", () => {
test("should process busy events and store them in cache", async () => {
const events: CalendarSubscriptionEventItem[] = [
{
id: "event-1",
iCalUID: "event-1@cal.com",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
busy: true,
summary: "Test Event",
description: "Test Description",
location: "Test Location",
status: "confirmed",
isAllDay: false,
timeZone: "UTC",
recurringEventId: null,
originalStartDate: null,
createdAt: new Date("2023-12-01T09:00:00Z"),
updatedAt: new Date("2023-12-01T09:30:00Z"),
etag: "test-etag",
kind: "calendar#event",
},
];
await service.handleEvents(mockSelectedCalendar, events);
expect(mockRepository.upsertMany).toHaveBeenCalledWith([
{
externalId: "event-1",
selectedCalendarId: "test-calendar-id",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
summary: "Test Event",
description: "Test Description",
location: "Test Location",
isAllDay: false,
timeZone: "UTC",
originalStartTime: null,
recurringEventId: null,
externalEtag: "test-etag",
externalCreatedAt: new Date("2023-12-01T09:00:00Z"),
externalUpdatedAt: new Date("2023-12-01T09:30:00Z"),
},
]);
expect(mockRepository.deleteMany).toHaveBeenCalledWith([]);
});
test("should not store free events in cache", async () => {
const events: CalendarSubscriptionEventItem[] = [
{
id: "event-1",
iCalUID: "event-1@cal.com",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
busy: false,
summary: "Free Event",
description: "Free Description",
location: "Free Location",
status: "confirmed",
isAllDay: false,
timeZone: "UTC",
recurringEventId: null,
originalStartDate: null,
createdAt: new Date("2023-12-01T09:00:00Z"),
updatedAt: new Date("2023-12-01T09:30:00Z"),
etag: "test-etag",
kind: "calendar#event",
},
];
await service.handleEvents(mockSelectedCalendar, events);
expect(mockRepository.upsertMany).toHaveBeenCalledWith([]);
expect(mockRepository.deleteMany).toHaveBeenCalledWith([
{
selectedCalendarId: "test-calendar-id",
externalId: "event-1",
},
]);
});
test("should delete cancelled events from cache", async () => {
const events: CalendarSubscriptionEventItem[] = [
{
id: "event-1",
iCalUID: "event-1@cal.com",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
busy: true,
summary: "Cancelled Event",
description: "Cancelled Description",
location: "Cancelled Location",
status: "cancelled",
isAllDay: false,
timeZone: "UTC",
recurringEventId: null,
originalStartDate: null,
createdAt: new Date("2023-12-01T09:00:00Z"),
updatedAt: new Date("2023-12-01T09:30:00Z"),
etag: "test-etag",
kind: "calendar#event",
},
];
await service.handleEvents(mockSelectedCalendar, events);
expect(mockRepository.upsertMany).toHaveBeenCalledWith([]);
expect(mockRepository.deleteMany).toHaveBeenCalledWith([
{
selectedCalendarId: "test-calendar-id",
externalId: "event-1",
},
]);
});
test("should handle mixed event types correctly", async () => {
const events: CalendarSubscriptionEventItem[] = [
{
id: "event-1",
iCalUID: "event-1@cal.com",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
busy: true,
summary: "Busy Event",
description: "Busy Description",
location: "Busy Location",
status: "confirmed",
isAllDay: false,
timeZone: "UTC",
recurringEventId: null,
originalStartDate: null,
createdAt: new Date("2023-12-01T09:00:00Z"),
updatedAt: new Date("2023-12-01T09:30:00Z"),
etag: "test-etag-1",
kind: "calendar#event",
},
{
id: "event-2",
iCalUID: "event-2@cal.com",
start: new Date("2023-12-01T12:00:00Z"),
end: new Date("2023-12-01T13:00:00Z"),
busy: false,
summary: "Free Event",
description: "Free Description",
location: "Free Location",
status: "confirmed",
isAllDay: false,
timeZone: "UTC",
recurringEventId: null,
originalStartDate: null,
createdAt: new Date("2023-12-01T11:00:00Z"),
updatedAt: new Date("2023-12-01T11:30:00Z"),
etag: "test-etag-2",
kind: "calendar#event",
},
{
id: "event-3",
iCalUID: "event-3@cal.com",
start: new Date("2023-12-01T14:00:00Z"),
end: new Date("2023-12-01T15:00:00Z"),
busy: true,
summary: "Cancelled Event",
description: "Cancelled Description",
location: "Cancelled Location",
status: "cancelled",
isAllDay: false,
timeZone: "UTC",
recurringEventId: null,
originalStartDate: null,
createdAt: new Date("2023-12-01T13:00:00Z"),
updatedAt: new Date("2023-12-01T13:30:00Z"),
etag: "test-etag-3",
kind: "calendar#event",
},
];
await service.handleEvents(mockSelectedCalendar, events);
expect(mockRepository.upsertMany).toHaveBeenCalledWith([
{
externalId: "event-1",
selectedCalendarId: "test-calendar-id",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
summary: "Busy Event",
description: "Busy Description",
location: "Busy Location",
isAllDay: false,
timeZone: "UTC",
originalStartTime: null,
recurringEventId: null,
externalEtag: "test-etag-1",
externalCreatedAt: new Date("2023-12-01T09:00:00Z"),
externalUpdatedAt: new Date("2023-12-01T09:30:00Z"),
},
]);
expect(mockRepository.deleteMany).toHaveBeenCalledWith([
{
selectedCalendarId: "test-calendar-id",
externalId: "event-2",
},
{
selectedCalendarId: "test-calendar-id",
externalId: "event-3",
},
]);
});
test("should handle empty events array", async () => {
await service.handleEvents(mockSelectedCalendar, []);
expect(mockRepository.upsertMany).toHaveBeenCalledWith([]);
expect(mockRepository.deleteMany).toHaveBeenCalledWith([]);
});
test("should handle events with missing optional fields", async () => {
const events: CalendarSubscriptionEventItem[] = [
{
id: "event-1",
iCalUID: "event-1@cal.com",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
busy: true,
summary: null,
description: null,
location: null,
status: "confirmed",
isAllDay: undefined,
timeZone: null,
recurringEventId: null,
originalStartDate: null,
createdAt: null,
updatedAt: null,
etag: null,
kind: null,
},
];
await service.handleEvents(mockSelectedCalendar, events);
expect(mockRepository.upsertMany).toHaveBeenCalledWith([
{
externalId: "event-1",
selectedCalendarId: "test-calendar-id",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
summary: null,
description: null,
location: null,
isAllDay: undefined,
timeZone: null,
originalStartTime: null,
recurringEventId: null,
externalEtag: "",
externalCreatedAt: null,
externalUpdatedAt: null,
},
]);
});
});
describe("cleanupCache", () => {
test("should delete all events for selected calendar", async () => {
await service.cleanupCache(mockSelectedCalendar);
expect(mockRepository.deleteAllBySelectedCalendarId).toHaveBeenCalledWith("test-calendar-id");
});
});
describe("cleanupStaleCache", () => {
test("should delete stale events", async () => {
await service.cleanupStaleCache();
expect(mockRepository.deleteStale).toHaveBeenCalled();
});
});
describe("isCalendarTypeSupported", () => {
test("should return true for supported calendar types", () => {
expect(CalendarCacheEventService.isCalendarTypeSupported("google_calendar")).toBe(true);
});
test("should return false for unsupported calendar types", () => {
expect(CalendarCacheEventService.isCalendarTypeSupported("office365_calendar")).toBe(false);
expect(CalendarCacheEventService.isCalendarTypeSupported("outlook_calendar")).toBe(false);
expect(CalendarCacheEventService.isCalendarTypeSupported("apple_calendar")).toBe(false);
expect(CalendarCacheEventService.isCalendarTypeSupported("unknown_calendar")).toBe(false);
});
test("should return false for null or undefined", () => {
expect(CalendarCacheEventService.isCalendarTypeSupported(null)).toBe(false);
expect(CalendarCacheEventService.isCalendarTypeSupported(undefined)).toBe(false);
});
});
});