Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions functions/api/feeds/__tests__/ical.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,72 @@ describe("GET /api/feeds/ical — day-aware performance_date (real-DB)", () => {
expect(idxLate).toBeLessThan(idxMorning);
});
});

// ---------------------------------------------------------------------------
// #601 — a set that STRADDLES midnight (23:30–00:30) must stamp DTEND with the
// next calendar day; same-date stamps would put DTEND before DTSTART, an
// invalid VEVENT. Distinct from pure after-midnight sets (start 00:00–05:59),
// which correctly keep both stamps on the stored date (covered above).
// ---------------------------------------------------------------------------
describe("GET /api/feeds/ical — midnight-straddling DTEND rolls to the next day (#601)", () => {
test("23:30–00:30 set: DTSTART on the stored date, DTEND on the following day", async () => {
const { env, rawDb } = createTestEnv();
env.PUBLIC_DATA_PUBLISH_ENABLED = "true";

const event = insertEvent(rawDb, {
name: "Straddle Ical Event",
slug: "ical-straddle",
date: "2099-03-01",
});
rawDb.prepare("UPDATE events SET is_published=1 WHERE id=?").run(event.id);
const venue = insertVenue(rawDb, { name: "Room 47" });

insertBand(rawDb, {
name: "Straddle Band",
event_id: event.id,
venue_id: venue.id,
start_time: "23:30",
end_time: "00:30",
});

const request = new Request("https://example.test/api/feeds/ical");
const response = await onRequestGet({ request, env });

expect(response.status).toBe(200);
const icalData = await response.text();

expect(icalData).toContain("DTSTART:20990301T233000");
expect(icalData).toContain("DTEND:20990302T003000");
expect(icalData).not.toContain("DTEND:20990301T003000");
});

test("month boundary: 23:00–00:15 on March 31 stamps DTEND April 1", async () => {
const { env, rawDb } = createTestEnv();
env.PUBLIC_DATA_PUBLISH_ENABLED = "true";

const event = insertEvent(rawDb, {
name: "Month Boundary Ical Event",
slug: "ical-month-boundary",
date: "2099-03-31",
});
rawDb.prepare("UPDATE events SET is_published=1 WHERE id=?").run(event.id);
const venue = insertVenue(rawDb, { name: "Princess Cafe" });

insertBand(rawDb, {
name: "Boundary Band",
event_id: event.id,
venue_id: venue.id,
start_time: "23:00",
end_time: "00:15",
});

const request = new Request("https://example.test/api/feeds/ical");
const response = await onRequestGet({ request, env });

expect(response.status).toBe(200);
const icalData = await response.text();

expect(icalData).toContain("DTSTART:20990331T230000");
expect(icalData).toContain("DTEND:20990401T001500");
});
});
21 changes: 20 additions & 1 deletion functions/api/feeds/ical.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,18 @@ function generateICal(bands, city, genre) {
const startTime = band.start_time || "20:00"; // HH:MM
const endTime = band.end_time || "21:00";

// A set that straddles midnight (e.g. 23:30–00:30) ENDS on the next
// calendar day — stamping both ends with the same date would put DTEND
// before DTSTART, an invalid VEVENT some clients reject (#601). Zero-padded
// HH:MM compares lexicographically; this mirrors prepareBands()'s
// `endMs < startMs` +1-day roll in frontend/src/utils/bandUtils.js. Pure
// after-midnight sets (start 00:00–05:59) are unaffected: both stamps
// correctly share the stored date.
const endDate = endTime < startTime ? nextCalendarDay(eventDate) : eventDate;

// Convert to iCal format (YYYYMMDDTHHMMSS)
const dtstart = `${eventDate.replace(/-/g, "")}T${startTime.replace(/:/g, "")}00`;
const dtend = `${eventDate.replace(/-/g, "")}T${endTime.replace(/:/g, "")}00`;
const dtend = `${endDate.replace(/-/g, "")}T${endTime.replace(/:/g, "")}00`;

// Generate unique ID using performance ID to ensure uniqueness per band
const uid = `performance-${band.performance_id}-${eventDate}@settimes.ca`;
Expand Down Expand Up @@ -145,6 +154,16 @@ function generateICal(bands, city, genre) {
return ical.join("\r\n");
}

/**
* YYYY-MM-DD → the following calendar day, DST-proof via UTC math (the
* string is a date literal, not a moment in time).
*/
function nextCalendarDay(dateStr) {
const next = new Date(`${dateStr}T00:00:00Z`);
next.setUTCDate(next.getUTCDate() + 1);
return next.toISOString().slice(0, 10);
}

function escapeIcal(text) {
if (!text) return "";
return text
Expand Down
Loading