import axios from 'axios';
import { WeatherService } from '../src/services/weather.js';
import { GeocodingService } from '../src/services/geocoding.js';
import { LocationService } from '../src/services/location.js';
jest.mock('axios');
jest.mock('../src/services/geocoding.js');
jest.mock('../src/services/location.js');
const mockedAxios = axios as jest.Mocked<typeof axios>;
const MockedGeocodingService = GeocodingService as jest.MockedClass<
typeof GeocodingService
>;
const MockedLocationService = LocationService as jest.MockedClass<
typeof LocationService
>;
describe('WeatherService', () => {
let weatherService: WeatherService;
let mockGeocodingService: jest.Mocked<GeocodingService>;
const mockApiKey = 'test-api-key';
beforeEach(() => {
jest.clearAllMocks();
mockGeocodingService = {
resolveLocation: jest.fn(),
getCoordinatesByLocationName: jest.fn(),
getCoordinatesByZipCode: jest.fn(),
} as any;
MockedGeocodingService.mockImplementation(() => mockGeocodingService);
weatherService = new WeatherService(mockApiKey);
});
const mockWeatherResponse = {
lat: 40.7128,
lon: -74.0060,
timezone: 'America/New_York',
timezone_offset: -18000,
current: {
dt: 1609459200,
sunrise: 1609422000,
sunset: 1609452000,
temp: 20,
feels_like: 18,
pressure: 1013,
humidity: 65,
dew_point: 15,
uvi: 3,
clouds: 40,
visibility: 10000,
wind_speed: 5,
wind_deg: 200,
weather: [
{
id: 800,
main: 'Clear',
description: 'clear sky',
icon: '01d',
},
],
},
hourly: [
{
dt: 1609459200,
temp: 20,
feels_like: 18,
pressure: 1013,
humidity: 65,
dew_point: 15,
uvi: 3,
clouds: 40,
visibility: 10000,
wind_speed: 5,
wind_deg: 200,
weather: [
{
id: 800,
main: 'Clear',
description: 'clear sky',
icon: '01d',
},
],
pop: 0.1,
},
],
daily: [
{
dt: 1609459200,
sunrise: 1609422000,
sunset: 1609452000,
moonrise: 1609440000,
moonset: 1609470000,
moon_phase: 0.5,
summary: 'Clear sky throughout the day',
temp: {
day: 20,
min: 15,
max: 25,
night: 18,
eve: 22,
morn: 16,
},
feels_like: {
day: 18,
night: 16,
eve: 20,
morn: 14,
},
pressure: 1013,
humidity: 65,
dew_point: 15,
wind_speed: 5,
wind_deg: 200,
weather: [
{
id: 800,
main: 'Clear',
description: 'clear sky',
icon: '01d',
},
],
clouds: 40,
pop: 0.1,
uvi: 3,
},
],
alerts: [
{
sender_name: 'NWS',
event: 'Heat Wave',
start: 1609459200,
end: 1609545600,
description: 'Excessive heat warning in effect',
tags: ['heat', 'warning'],
},
],
};
describe('getCurrentWeather', () => {
it('should fetch current weather for a location', async () => {
mockGeocodingService.resolveLocation.mockResolvedValueOnce({
lat: 40.7128,
lon: -74.0060,
});
mockedAxios.get.mockResolvedValueOnce({ data: mockWeatherResponse });
const location = { city: 'New York' };
const result = await weatherService.getCurrentWeather(location);
expect(result).toEqual({
weather: mockWeatherResponse,
detectedLocation: undefined,
});
expect(mockGeocodingService.resolveLocation).toHaveBeenCalledWith(location);
expect(mockedAxios.get).toHaveBeenCalledWith(
'https://api.openweathermap.org/data/3.0/onecall',
{
params: {
lat: 40.7128,
lon: -74.0060,
appid: mockApiKey,
units: "metric",
},
}
);
});
it('should include options in API call', async () => {
mockGeocodingService.resolveLocation.mockResolvedValueOnce({
lat: 40.7128,
lon: -74.0060,
});
mockedAxios.get.mockResolvedValueOnce({ data: mockWeatherResponse });
const location = { city: 'New York' };
const options = { units: 'imperial' as const, lang: 'en' };
await weatherService.getCurrentWeather(location, options);
expect(mockedAxios.get).toHaveBeenCalledWith(
'https://api.openweathermap.org/data/3.0/onecall',
{
params: {
lat: 40.7128,
lon: -74.0060,
appid: mockApiKey,
units: 'imperial',
lang: 'en',
},
}
);
});
});
describe('getWeatherByCoordinates', () => {
it('should fetch weather by coordinates', async () => {
mockedAxios.get.mockResolvedValueOnce({ data: mockWeatherResponse });
const result = await weatherService.getWeatherByCoordinates(40.7128, -74.0060);
expect(result).toEqual(mockWeatherResponse);
expect(mockedAxios.get).toHaveBeenCalledWith(
'https://api.openweathermap.org/data/3.0/onecall',
{
params: {
lat: 40.7128,
lon: -74.0060,
appid: mockApiKey,
units: "metric",
},
}
);
});
it('should handle 401 authentication error', async () => {
mockedAxios.get.mockRejectedValueOnce({
isAxiosError: true,
response: { status: 401 },
});
mockedAxios.isAxiosError.mockReturnValueOnce(true);
await expect(
weatherService.getWeatherByCoordinates(40.7128, -74.0060)
).rejects.toThrow('Invalid API key or insufficient permissions for One Call API 3.0');
});
it('should handle 400 bad request error', async () => {
mockedAxios.get.mockRejectedValueOnce({
isAxiosError: true,
response: { status: 400 },
});
mockedAxios.isAxiosError.mockReturnValueOnce(true);
await expect(
weatherService.getWeatherByCoordinates(999, 999)
).rejects.toThrow('Invalid coordinates: lat=999, lon=999');
});
});
describe('getHourlyForecast', () => {
it('should fetch hourly forecast', async () => {
mockGeocodingService.resolveLocation.mockResolvedValueOnce({
lat: 40.7128,
lon: -74.0060,
});
mockedAxios.get.mockResolvedValueOnce({ data: mockWeatherResponse });
const location = { city: 'New York' };
const result = await weatherService.getHourlyForecast(location, 12);
expect(result).toEqual({
forecast: mockWeatherResponse.hourly.slice(0, 12),
detectedLocation: undefined,
});
});
});
describe('getDailyForecast', () => {
it('should fetch daily forecast', async () => {
mockGeocodingService.resolveLocation.mockResolvedValueOnce({
lat: 40.7128,
lon: -74.0060,
});
mockedAxios.get.mockResolvedValueOnce({ data: mockWeatherResponse });
const location = { city: 'New York' };
const result = await weatherService.getDailyForecast(location, 5);
expect(result).toEqual({
forecast: mockWeatherResponse.daily.slice(0, 5),
detectedLocation: undefined,
});
});
});
describe('getWeatherAlerts', () => {
it('should fetch weather alerts', async () => {
mockGeocodingService.resolveLocation.mockResolvedValueOnce({
lat: 40.7128,
lon: -74.0060,
});
mockedAxios.get.mockResolvedValueOnce({ data: mockWeatherResponse });
const location = { city: 'New York' };
const result = await weatherService.getWeatherAlerts(location);
expect(result).toEqual({
alerts: mockWeatherResponse.alerts,
detectedLocation: undefined,
});
});
it('should return empty array when no alerts', async () => {
mockGeocodingService.resolveLocation.mockResolvedValueOnce({
lat: 40.7128,
lon: -74.0060,
});
const responseWithoutAlerts: Omit<typeof mockWeatherResponse, 'alerts'> = {
...mockWeatherResponse,
};
delete (responseWithoutAlerts as any).alerts;
mockedAxios.get.mockResolvedValueOnce({ data: responseWithoutAlerts });
const location = { city: 'New York' };
const result = await weatherService.getWeatherAlerts(location);
expect(result).toEqual({
alerts: [],
detectedLocation: undefined,
});
});
});
});