Files
homarr/packages/request-handler/src/weather.spec.ts
T

113 lines
4.3 KiB
TypeScript

import { Response } from "undici";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { weatherRequestHandler } from "./weather";
vi.mock("@homarr/core/infrastructure/http", () => ({
fetchWithTrustedCertificatesAsync: vi.fn(),
}));
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const makeWeatherResponse = () => {
const hours = Array.from({ length: 25 }, (_, index) => `2026-07-01T${String(index % 24).padStart(2, "0")}:00`);
const days = [
"2026-06-30",
...Array.from({ length: 7 }, (_, index) => `2026-07-${String(index + 1).padStart(2, "0")}`),
];
const hourlyValues = hours.map<number | null>((_, index) => index);
const dailyValues = days.map<number | null>((_, index) => index);
const sunrise = days.map<string | null>((day) => `${day}T05:00`);
return {
timezone: "Europe/Berlin",
utc_offset_seconds: 7200,
current: {
time: "2026-07-01T12:00",
temperature_2m: 24,
relative_humidity_2m: 55,
apparent_temperature: 25,
is_day: 1,
precipitation: 0,
weather_code: 1,
wind_speed_10m: 12,
wind_gusts_10m: 20,
},
hourly: {
time: hours,
temperature_2m: hourlyValues,
apparent_temperature: hourlyValues,
precipitation_probability: hourlyValues,
precipitation: hourlyValues,
weather_code: hourlyValues,
relative_humidity_2m: hourlyValues,
wind_speed_10m: hourlyValues,
wind_gusts_10m: hourlyValues,
},
daily: {
time: days,
weather_code: dailyValues,
temperature_2m_max: dailyValues,
temperature_2m_min: dailyValues,
apparent_temperature_max: dailyValues,
apparent_temperature_min: dailyValues,
precipitation_probability_max: dailyValues,
precipitation_sum: dailyValues,
relative_humidity_2m_mean: dailyValues,
wind_speed_10m_max: dailyValues,
wind_gusts_10m_max: dailyValues,
uv_index_max: dailyValues,
sunrise,
sunset: days.map((day) => `${day}T21:00`),
daylight_duration: dailyValues,
},
};
};
beforeEach(() => {
vi.clearAllMocks();
weatherRequestHandler.invalidateCache();
});
describe("weather request handler", () => {
it("requests bounded useful forecasts and normalizes location times", async () => {
const response = makeWeatherResponse();
response.hourly.temperature_2m = [null];
Reflect.deleteProperty(response.hourly, "wind_gusts_10m");
response.daily.sunrise[1] = null;
mockFetch.mockResolvedValue(new Response(JSON.stringify(response)));
const first = await weatherRequestHandler.handler({ latitude: 48.85, longitude: 2.35 }).getDataAsync();
const second = await weatherRequestHandler.handler({ latitude: 48.85, longitude: 2.35 }).getDataAsync();
expect(first.data.current.observedAt).toBe("2026-07-01T10:00:00.000Z");
expect(first.data.hourly).toHaveLength(24);
expect(first.data.daily).toHaveLength(7);
expect(first.data.daily[0]?.date).toBe("2026-07-01");
expect(first.data.hourly[0]?.temperature).toBeNull();
expect(first.data.hourly[1]?.temperature).toBeNull();
expect(first.data.hourly[0]?.windGusts).toBeNull();
expect(first.data.daily[0]?.sunriseAt).toBeNull();
expect(first.data.daily[1]?.sunriseAt).toBe("2026-07-02T03:00:00.000Z");
expect(second.data).toStrictEqual(first.data);
expect(mockFetch).toHaveBeenCalledOnce();
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
expect(requestUrl.searchParams.get("forecast_hours")).toBe("24");
expect(requestUrl.searchParams.get("forecast_days")).toBe("7");
expect(requestUrl.searchParams.has("past_days")).toBe(false);
expect(requestUrl.searchParams.get("timezone")).toBe("auto");
expect(requestUrl.searchParams.get("current")).toContain("apparent_temperature");
expect(requestUrl.searchParams.get("hourly")).toContain("precipitation_probability");
expect(requestUrl.searchParams.get("daily")).toContain("uv_index_max");
});
it("rejects unsuccessful Open-Meteo responses", async () => {
mockFetch.mockResolvedValue(new Response("unavailable", { status: 503 }));
await expect(weatherRequestHandler.handler({ latitude: 48.85, longitude: 2.35 }).getDataAsync()).rejects.toThrow();
});
});