forked from GitHubMirrors/silverbullet-icalendar
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import { assertEquals, assert } from "jsr:@std/assert";
|
|
import { resolveEventStart, expandRecurrences, localDateString } from "../icalendar.ts";
|
|
|
|
const TEST_NOW = new Date("2026-01-20T12:00:00Z");
|
|
|
|
Deno.test("Variation: Standard Opaque Meeting (Busy)", async () => {
|
|
const icsEvent = {
|
|
summary: "Discuss Alletra MP terraform provider requirements",
|
|
start: {
|
|
date: "2026-01-16T15:30:00.000",
|
|
local: {
|
|
date: "2026-01-16T15:30:00.000",
|
|
timezone: "GMT Standard Time"
|
|
}
|
|
},
|
|
transp: "OPAQUE",
|
|
"x-microsoft-cdo-busystatus": "BUSY"
|
|
};
|
|
|
|
const result = await resolveEventStart(icsEvent);
|
|
// GMT Standard Time in Jan is UTC+0
|
|
assertEquals(result?.toISOString(), "2026-01-16T15:30:00.000Z");
|
|
});
|
|
|
|
Deno.test("Variation: Transparent Meeting (Free)", async () => {
|
|
const icsEvent = {
|
|
summary: "Following: Neutron Star Program Meeting",
|
|
start: {
|
|
date: "2026-01-20T08:30:00.000",
|
|
local: {
|
|
date: "2026-01-20T08:30:00.000",
|
|
timezone: "Pacific Standard Time"
|
|
}
|
|
},
|
|
transp: "TRANSPARENT",
|
|
"x-microsoft-cdo-busystatus": "FREE"
|
|
};
|
|
|
|
const result = await resolveEventStart(icsEvent);
|
|
// PST in Jan is UTC-8
|
|
assertEquals(result?.toISOString(), "2026-01-20T16:30:00.000Z");
|
|
});
|
|
|
|
Deno.test("Variation: Recurring Weekly (Multi-day: MO,TU,WE,TH,FR)", () => {
|
|
const icsEvent = {
|
|
summary: "BUSY Weekly",
|
|
start: "2026-01-16T13:00:00",
|
|
rrule: "FREQ=WEEKLY;UNTIL=20260814T170000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;WKST=SU"
|
|
};
|
|
|
|
// Use TEST_NOW to ensure the window matches
|
|
const results = expandRecurrences(icsEvent, 7, TEST_NOW);
|
|
// Should have multiple occurrences per week
|
|
assert(results.length > 1);
|
|
assert(results.some(r => r.start.includes("2026-01-19"))); // Monday
|
|
assert(results.some(r => r.start.includes("2026-01-20"))); // Tuesday
|
|
});
|
|
|
|
Deno.test("Variation: Recurring with EXDATE (Exclusion)", () => {
|
|
const icsEvent = {
|
|
summary: "HPE-Veeam check-in",
|
|
start: "2026-01-20T08:30:00",
|
|
rrule: "FREQ=WEEKLY;UNTIL=20260324T143000Z;INTERVAL=1;BYDAY=TU;WKST=SU",
|
|
exdate: ["2026-02-03T08:30:00"]
|
|
};
|
|
|
|
const results = expandRecurrences(icsEvent, 60, TEST_NOW);
|
|
const dates = results.map(r => r.start);
|
|
assert(dates.includes("2026-01-20T08:30:00"));
|
|
assert(dates.includes("2026-01-27T08:30:00"));
|
|
assert(!dates.includes("2026-02-03T08:30:00"), "EXDATE should be excluded");
|
|
assert(dates.includes("2026-02-10T08:30:00"));
|
|
});
|
|
|
|
Deno.test("Variation: Monthly Recurring (Last Friday)", () => {
|
|
const icsEvent = {
|
|
summary: "Monthly Planning",
|
|
start: "2026-01-30T10:00:00", // This is the last Friday of Jan 2026
|
|
rrule: "FREQ=MONTHLY;UNTIL=20260731T170000Z;INTERVAL=1;BYDAY=-1FR"
|
|
};
|
|
|
|
const results = expandRecurrences(icsEvent, 100, TEST_NOW);
|
|
const dates = results.map(r => r.start);
|
|
|
|
assert(dates.includes("2026-01-30T10:00:00"));
|
|
assert(dates.includes("2026-02-27T10:00:00")); // Last Friday of Feb 2026
|
|
assert(dates.includes("2026-03-27T10:00:00")); // Last Friday of Mar 2026
|
|
});
|
|
|
|
Deno.test("Variation: Tentative Meeting", async () => {
|
|
const icsEvent = {
|
|
summary: "CO SW&P: Morpheus SW Core Team",
|
|
start: {
|
|
date: "2026-01-19T11:30:00.000",
|
|
local: {
|
|
date: "2026-01-19T11:30:00.000",
|
|
timezone: "Central Standard Time"
|
|
}
|
|
},
|
|
"x-microsoft-cdo-busystatus": "TENTATIVE"
|
|
};
|
|
|
|
const result = await resolveEventStart(icsEvent);
|
|
// CST in Jan is UTC-6
|
|
assertEquals(result?.toISOString(), "2026-01-19T17:30:00.000Z");
|
|
});
|
|
|
|
Deno.test("Variation: Long Location/URL", () => {
|
|
const icsEvent = {
|
|
summary: "Omnissa Horizon:HPE VME Weekly Cadence",
|
|
location: "https://omnissa.zoom.us/j/84780526943?pwd=fow88EiiZyUKsW26JrJavqiirbb1hv.1&from=addon"
|
|
};
|
|
assertEquals(icsEvent.location.length > 50, true);
|
|
});
|
|
|
|
Deno.test("Feature: Unlimited lookback window", () => {
|
|
const start = new Date(TEST_NOW.getTime() - 500 * 86400000); // 500 days ago
|
|
const icsEvent = {
|
|
summary: "Event from 500 days ago",
|
|
start: localDateString(start),
|
|
rrule: "FREQ=DAILY;COUNT=1000"
|
|
};
|
|
|
|
const results = expandRecurrences(icsEvent, 30, TEST_NOW);
|
|
// Should include events from 500 days ago because there is now no limit
|
|
assert(results.some(r => r.start === localDateString(start)), "Should find occurrence from 500 days ago");
|
|
});
|
|
|
|
Deno.test("Feature: Hash Collision Prevention (Same UID/Start, Different Summary)", async () => {
|
|
// This happens in reachcalendar.ics where a "Following:" event shares UID/Time with main event
|
|
const event1 = {
|
|
start: "2026-01-20T08:30:00",
|
|
uid: "collision-uid",
|
|
summary: "Main Meeting"
|
|
};
|
|
const event2 = {
|
|
start: "2026-01-20T08:30:00",
|
|
uid: "collision-uid",
|
|
summary: "Following: Main Meeting"
|
|
};
|
|
|
|
const hash1 = await sha256Hash(`${event1.start}${event1.uid}${event1.summary}`);
|
|
const hash2 = await sha256Hash(`${event2.start}${event2.uid}${event2.summary}`);
|
|
|
|
assert(hash1 !== hash2, "Hashes must be unique even if UID and Start match");
|
|
});
|
|
|
|
// Helper needed for the test above since it's not exported from icalendar.ts
|
|
async function sha256Hash(str: string): Promise<string> {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(str);
|
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
|
|
}
|