Files
ridebus-board/test/weather.test.mjs
2026-10-08 22:56:41 +03:00

40 lines
3.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createWeatherClient } from '../server/weather.mjs';
import { weatherAvailable, weatherCondition } from '../src/lib/weather.mjs';
const now = Date.parse('2026-10-08T12:00:00+03:00');
const raw = { current: { time: now / 1000, temperature_2m: 12.2, apparent_temperature: 10.5, weather_code: 61, is_day: 1, wind_speed_10m: 3 },
hourly: { time: [now / 1000 + 3600], temperature_2m: [13], weather_code: [3], precipitation_probability: [20] } };
test('weather requests are coalesced and exclude transit credentials; Unix times retain Minsk instant', async () => {
let calls = 0;
const client = createWeatherClient({ now: () => now, fetchData: async (url, init) => {
calls++; assert.equal(url.searchParams.get('wind_speed_unit'), 'ms'); assert.equal(url.searchParams.get('timeformat'), 'unixtime');
assert.deepEqual(init.headers, { Accept: 'application/json' }); return Response.json(raw);
} });
const [first, second] = await Promise.all([client.weather(1, 1, { latitude: 55.5, longitude: 28.7 }), client.weather(1, 1, { latitude: 55.5, longitude: 28.7 })]);
assert.deepEqual(first, second); assert.equal(calls, 1); assert.equal(first.observedAt, new Date(now).toISOString());
assert.equal(weatherAvailable(first, now + 30 * 60000), true); assert.equal(weatherAvailable(first, now + 61 * 60000), false);
assert.equal(weatherCondition(first.code).label, 'Дождь');
});
test('missing coordinates and null weather values never produce a fabricated 0 degree forecast', async () => {
const client = createWeatherClient({ now: () => now, fetchData: async () => Response.json({ ...raw, current: { ...raw.current, temperature_2m: null } }) });
await assert.rejects(client.weather(1, 1, { latitude: null, longitude: null }), error => error.code === 'coordinates_unavailable');
await assert.rejects(client.weather(1, 1, { latitude: 55, longitude: 28 }), error => error.code === 'weather_unavailable');
});
test('commercial provider key selects its endpoint and is never exposed in response or errors', async () => {
const client = createWeatherClient({ apiKey: 'private-weather-key', now: () => now, fetchData: async url => {
assert.equal(url.host, 'customer-api.open-meteo.com'); assert.equal(url.searchParams.get('apikey'), 'private-weather-key'); return Response.json(raw);
} });
assert.ok(!JSON.stringify(await client.weather(1, 1, { latitude: 55, longitude: 28 })).includes('private-weather-key'));
});
test('provider outage backs off for a minute without extending the old forecast timestamp', async () => {
let current = now, calls = 0;
const client = createWeatherClient({ now: () => current, fetchData: async () => { calls++; return calls === 1 ? Response.json(raw) : new Response('', { status: 503 }); } });
const coords = { latitude: 55, longitude: 28 };
const saved = await client.weather(1, 1, coords);
current += 11 * 60000;
await assert.rejects(client.weather(1, 1, coords)); await assert.rejects(client.weather(1, 1, coords));
assert.equal(calls, 2); assert.equal(saved.updatedAt, new Date(now).toISOString());
});