Files
TREK/server/tests/unit/nest/weather.controller.test.ts
T
Maurice 2c3b20bb57 feat(tours): paddle portages and locks, paddle time, wind at the start and obstacle data from OpenStreetMap
PAD-03 portages and locks on planned tours and recorded tracks, PAD-07 paddle time with portage and lock stops, PAD-08 wind forecast for the start, PAD-05A regional obstacle import from Geofabrik extracts with admin endpoints and a monthly refresh, PAD-05B first cut of the obstacle layer on Leaflet, GL and 3D. Work in progress: checklist (PAD-06), check bench (PAD-09), admin UI, translations and tests follow.
2026-10-16 23:44:58 +02:00

106 lines
4.5 KiB
TypeScript

import { WeatherController } from '../../../src/nest/weather/weather.controller';
import { ApiError } from '../../../src/nest/weather/weather.impl';
import type { WeatherService } from '../../../src/nest/weather/weather.service';
import type { WindService } from '../../../src/nest/weather/wind.service';
import { HttpException } from '@nestjs/common';
import { describe, it, expect, vi } from 'vitest';
function makeController(svc: Partial<WeatherService>, wind: Partial<WindService> = {}) {
return new WeatherController(svc as WeatherService, wind as WindService);
}
/** Run `fn`, expecting it to throw an HttpException; return its { status, body }. */
async function thrown(fn: () => Promise<unknown>): Promise<{ status: number; body: unknown }> {
try {
await fn();
} catch (err) {
expect(err).toBeInstanceOf(HttpException);
const e = err as HttpException;
return { status: e.getStatus(), body: e.getResponse() };
}
throw new Error('expected the handler to throw');
}
describe('WeatherController GET /api/weather/wind (PAD-08)', () => {
it('hands the validated query to the wind forecast and answers with it', async () => {
const answer = { status: 'past', hours: [], day: null };
const forecast = vi.fn().mockResolvedValue(answer);
const c = makeController({}, { forecast });
expect(await c.getWind({ lat: 47.5, lng: 9.7, date: '2026-10-18' })).toBe(answer);
expect(forecast).toHaveBeenCalledWith({ lat: 47.5, lng: 9.7, date: '2026-10-18' });
});
});
describe('WeatherController (parity with the legacy /api/weather route)', () => {
const sample = { temp: 21, main: 'Clear', description: 'Klar', type: 'current' };
describe('GET /api/weather', () => {
it('400 { error } with the exact legacy message when lat/lng missing', async () => {
const c = makeController({ get: vi.fn() });
expect(await thrown(() => c.getWeather(undefined, '13.4'))).toEqual({
status: 400,
body: { error: 'Latitude and longitude are required' },
});
});
it('returns the service result and defaults lang to "de" when absent', async () => {
const get = vi.fn().mockResolvedValue(sample);
const c = makeController({ get });
const res = await c.getWeather('52.5', '13.4', undefined, undefined);
expect(res).toEqual(sample);
expect(get).toHaveBeenCalledWith('52.5', '13.4', undefined, 'de', undefined);
});
it('passes an explicit lang and date through unchanged', async () => {
const get = vi.fn().mockResolvedValue(sample);
const c = makeController({ get });
await c.getWeather('1', '2', '2026-07-01', 'en');
expect(get).toHaveBeenCalledWith('1', '2', '2026-07-01', 'en', undefined);
});
it('maps an ApiError to its status + { error: message }', async () => {
const c = makeController({ get: vi.fn().mockRejectedValue(new ApiError(404, 'Open-Meteo API error')) });
expect(await thrown(() => c.getWeather('1', '2'))).toEqual({
status: 404,
body: { error: 'Open-Meteo API error' },
});
});
it('maps an unexpected error to the exact legacy 500 body', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {});
const c = makeController({ get: vi.fn().mockRejectedValue(new Error('boom')) });
expect(await thrown(() => c.getWeather('1', '2'))).toEqual({
status: 500,
body: { error: 'Error fetching weather data' },
});
});
});
describe('GET /api/weather/detailed', () => {
it('400 { error } with the exact legacy message when date missing', async () => {
const c = makeController({ getDetailed: vi.fn() });
expect(await thrown(() => c.getDetailed('1', '2', undefined))).toEqual({
status: 400,
body: { error: 'Latitude, longitude, and date are required' },
});
});
it('returns the detailed result and defaults lang to "de"', async () => {
const getDetailed = vi.fn().mockResolvedValue(sample);
const c = makeController({ getDetailed });
await c.getDetailed('1', '2', '2026-07-01', undefined);
expect(getDetailed).toHaveBeenCalledWith('1', '2', '2026-07-01', 'de');
});
it('maps an unexpected error to the exact detailed 500 body', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {});
const c = makeController({ getDetailed: vi.fn().mockRejectedValue(new Error('boom')) });
expect(await thrown(() => c.getDetailed('1', '2', '2026-07-01'))).toEqual({
status: 500,
body: { error: 'Error fetching detailed weather data' },
});
});
});
});