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
7 changes: 7 additions & 0 deletions defaultmodules/calendar/calendarfetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ class CalendarFetcher {
*/
async #handleResponse (response) {
try {
// 304 Not Modified has no body: keep previously fetched events and just re-broadcast them.
if (response.status === 304) {
this.lastFetch = Date.now();
this.broadcastEvents();
return;
}

const responseData = await response.text();
const parsed = await ical.async.parseICS(responseData);

Expand Down
6 changes: 6 additions & 0 deletions defaultmodules/newsfeed/newsfeedfetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class NewsfeedFetcher {
* @param {Response} response - The fetch Response object
*/
async #handleResponse (response) {
// 304 Not Modified has no body: keep previously fetched items and re-broadcast them.
if (response.status === 304) {
this.broadcastItems();
return;
}

this.items = [];
const parser = new FeedMe();

Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/buienradar.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class BuienradarProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
this.#handleResponse(data);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/envcanada.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class EnvCanadaProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
// Check if hour changed - restart fetcher with new URL
const newHour = new Date().toISOString().substring(11, 13);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/openmeteo.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ class OpenMeteoProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
this.#handleResponse(data);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/openweathermap.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class OpenWeatherMapProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
this.#handleResponse(data);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/pirateweather.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class PirateweatherProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
this.#handleResponse(data);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/smhi.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class SMHIProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
this.#handleResponse(data);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/ukmetofficedatahub.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class UkMetOfficeDataHubProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
this.#handleResponse(data);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/weatherflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class WeatherFlowProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
const processed = this.#processData(data);
Expand Down
1 change: 1 addition & 0 deletions defaultmodules/weather/providers/weathergov.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ class WeatherGovProvider {
});

this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
this.#handleResponse(data);
Expand Down
130 changes: 130 additions & 0 deletions tests/unit/modules/default/calendar/calendar_fetcher_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
global.moment = require("moment-timezone");

const ical = require("node-ical");
const moment = require("moment-timezone");
const defaults = require("../../../../../js/defaults");

const CalendarFetcherUtils = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcherutils`);

const CalendarFetcher = require(`../../../../../${defaults.defaultModulesDir}/calendar/calendarfetcher`);

const makeFetcher = (options = {}) => new CalendarFetcher(
options.url ?? "http://test.example.com/cal.ics",
options.reloadInterval ?? 60000,
options.excludedEvents ?? [],
options.maximumEntries ?? 10,
options.maximumNumberOfDays ?? 365,
options.auth ?? null,
options.includePastEvents ?? false,
options.selfSignedCert ?? false
);

// Triggers a fetch and resolves once the fetcher finishes (success or error).
// On error, resolves with the errorInfo object so tests can inspect it.
const emitResponse = (fetcher, response) => new Promise((resolve) => {
fetcher.onReceive(resolve);
fetcher.onError((_, errorInfo) => resolve(errorInfo));
fetcher.httpFetcher.emit("response", response);
});

const futureEventICS = () => {
const start = moment().add(1, "hour");
const end = moment().add(2, "hours");
return [
"BEGIN:VCALENDAR",
"BEGIN:VEVENT",
`DTSTART:${start.utc().format("YYYYMMDDTHHmmss")}Z`,
`DTEND:${end.utc().format("YYYYMMDDTHHmmss")}Z`,
"UID:future-1@test",
"SUMMARY:Future Event",
"END:VEVENT",
"END:VCALENDAR"
].join("\r\n");
};

describe("CalendarFetcher", () => {
afterEach(() => {
vi.restoreAllMocks();
});

describe("304 handling", () => {
it("keeps previously fetched events when a 304 Not Modified response arrives", async () => {
const fetcher = makeFetcher();

await emitResponse(fetcher, new Response(futureEventICS(), { status: 200 }));
expect(fetcher.events).toHaveLength(1);

// 304 Not Modified has an empty body: events must be preserved
await emitResponse(fetcher, new Response(null, { status: 304 }));
expect(fetcher.events).toHaveLength(1);
});
});

describe("error handling", () => {
it("forwards HTTP fetch errors to onError callback", () => {
const fetcher = makeFetcher();
const onError = vi.fn();
const errorInfo = { errorType: "NETWORK_ERROR", message: "boom" };

fetcher.onError(onError);
fetcher.httpFetcher.emit("error", errorInfo);

expect(onError).toHaveBeenCalledWith(fetcher, errorInfo);
});

it("keeps existing events and reports PARSE_ERROR when parsing fails", async () => {
const fetcher = makeFetcher();

await emitResponse(fetcher, new Response(futureEventICS(), { status: 200 }));
expect(fetcher.events).toHaveLength(1);

vi.spyOn(ical.async, "parseICS").mockRejectedValueOnce(new Error("invalid ics"));
const error = await emitResponse(fetcher, new Response("BROKEN", { status: 200 }));

expect(fetcher.events).toHaveLength(1);
expect(error).toMatchObject({
errorType: "PARSE_ERROR",
translationKey: "MODULE_ERROR_UNSPECIFIED",
url: "http://test.example.com/cal.ics"
});
});
});

describe("delegation and refetch", () => {
it("delegates fetchCalendar to HTTPFetcher.startPeriodicFetch", () => {
const fetcher = makeFetcher();
const startSpy = vi.spyOn(fetcher.httpFetcher, "startPeriodicFetch");

fetcher.fetchCalendar();

expect(startSpy).toHaveBeenCalledTimes(1);
});

it("shouldRefetch respects reload interval boundaries", () => {
const fetcher = makeFetcher();

expect(fetcher.shouldRefetch()).toBe(true);

fetcher.lastFetch = Date.now() - 59999;
expect(fetcher.shouldRefetch()).toBe(false);

fetcher.lastFetch = Date.now() - 60000;
expect(fetcher.shouldRefetch()).toBe(true);
});

it("passes configured filter options to CalendarFetcherUtils.filterEvents", async () => {
const excludedEvents = ["Do not show me"];
const filterSpy = vi.spyOn(CalendarFetcherUtils, "filterEvents");
const fetcher = makeFetcher({ excludedEvents, maximumEntries: 7, maximumNumberOfDays: 30, includePastEvents: true });

await emitResponse(fetcher, new Response(futureEventICS(), { status: 200 }));

expect(filterSpy).toHaveBeenCalledWith(expect.any(Object), {
excludedEvents,
includePastEvents: true,
maximumEntries: 7,
maximumNumberOfDays: 30
});
});
});
});