import axios from 'axios';
import { LocationService } from '../src/services/location.js';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('LocationService', () => {
let locationService: LocationService;
beforeEach(() => {
locationService = new LocationService();
jest.clearAllMocks();
});
describe('detectLocationFromIP', () => {
it('should detect location from IP successfully', async () => {
const mockResponse = {
ip: '203.0.113.1',
city: 'New York',
region: 'New York',
country: 'US',
country_name: 'United States',
continent_code: 'NA',
latitude: 40.7128,
longitude: -74.0060,
timezone: 'America/New_York',
currency: 'USD',
languages: 'en',
};
mockedAxios.get.mockResolvedValueOnce({ data: mockResponse });
const result = await locationService.detectLocationFromIP();
expect(result).toEqual({
lat: 40.7128,
lon: -74.0060,
city: 'New York',
region: 'New York',
country: 'United States',
source: 'ip',
});
expect(mockedAxios.get).toHaveBeenCalledWith(
'https://ipapi.co/json/',
{
timeout: 5000,
headers: {
'User-Agent': 'OpenWeatherMap-MCP-Server/1.0',
},
}
);
});
it('should handle missing optional fields', async () => {
const mockResponse = {
ip: '203.0.113.1',
city: 'Paris',
country: 'FR',
country_name: 'France',
latitude: 48.8566,
longitude: 2.3522,
timezone: 'Europe/Paris',
currency: 'EUR',
languages: 'fr',
// Missing region field
};
mockedAxios.get.mockResolvedValueOnce({ data: mockResponse });
const result = await locationService.detectLocationFromIP();
expect(result).toEqual({
lat: 48.8566,
lon: 2.3522,
city: 'Paris',
region: '',
country: 'France',
source: 'ip',
});
});
it('should handle response with missing city', async () => {
const mockResponse = {
ip: '203.0.113.1',
region: 'California',
country: 'US',
country_name: 'United States',
latitude: 37.7749,
longitude: -122.4194,
timezone: 'America/Los_Angeles',
currency: 'USD',
languages: 'en',
// Missing city field
};
mockedAxios.get.mockResolvedValueOnce({ data: mockResponse });
const result = await locationService.detectLocationFromIP();
expect(result).toEqual({
lat: 37.7749,
lon: -122.4194,
city: 'Unknown',
region: 'California',
country: 'United States',
source: 'ip',
});
});
it('should handle invalid location data (missing coordinates)', async () => {
const mockResponse = {
ip: '203.0.113.1',
city: 'Invalid City',
country: 'XX',
// Missing latitude and longitude
};
mockedAxios.get.mockResolvedValueOnce({ data: mockResponse });
await expect(locationService.detectLocationFromIP()).rejects.toThrow(
'Invalid location data received from IP service'
);
});
it('should handle network timeout', async () => {
const timeoutError = new Error('timeout');
timeoutError.name = 'Error';
(timeoutError as any).code = 'ECONNABORTED';
mockedAxios.get.mockRejectedValueOnce(timeoutError);
mockedAxios.isAxiosError.mockReturnValueOnce(true);
await expect(locationService.detectLocationFromIP()).rejects.toThrow(
'Location detection timed out. Please specify your location manually.'
);
});
it('should handle rate limiting (429 status)', async () => {
const rateLimitError = {
isAxiosError: true,
response: { status: 429 },
message: 'Too Many Requests',
};
mockedAxios.get.mockRejectedValueOnce(rateLimitError);
mockedAxios.isAxiosError.mockReturnValueOnce(true);
await expect(locationService.detectLocationFromIP()).rejects.toThrow(
'Location service rate limit exceeded. Please specify your location manually.'
);
});
it('should handle generic network errors', async () => {
const networkError = new Error('Network Error');
mockedAxios.get.mockRejectedValueOnce(networkError);
mockedAxios.isAxiosError.mockReturnValueOnce(true);
await expect(locationService.detectLocationFromIP()).rejects.toThrow(
'Failed to detect location automatically: Network Error. Please specify your location manually.'
);
});
it('should handle unknown errors', async () => {
mockedAxios.get.mockRejectedValueOnce('Unknown error');
await expect(locationService.detectLocationFromIP()).rejects.toThrow(
'Failed to detect location automatically: Unknown error. Please specify your location manually.'
);
});
});
describe('formatLocationMessage', () => {
it('should format location message with region', () => {
const detectedLocation = {
lat: 40.7128,
lon: -74.0060,
city: 'New York',
region: 'New York',
country: 'United States',
source: 'ip' as const,
};
const result = locationService.formatLocationMessage(detectedLocation);
expect(result).toBe(
'š **Detected location:** New York, New York, United States (approximate)\nš” *For more accurate weather, specify your exact location using city name, address, or coordinates.*'
);
});
it('should format location message without region', () => {
const detectedLocation = {
lat: 48.8566,
lon: 2.3522,
city: 'Paris',
region: '',
country: 'France',
source: 'ip' as const,
};
const result = locationService.formatLocationMessage(detectedLocation);
expect(result).toBe(
'š **Detected location:** Paris, France (approximate)\nš” *For more accurate weather, specify your exact location using city name, address, or coordinates.*'
);
});
it('should format location message with undefined region', () => {
const detectedLocation = {
lat: 48.8566,
lon: 2.3522,
city: 'Paris',
region: undefined as any,
country: 'France',
source: 'ip' as const,
};
const result = locationService.formatLocationMessage(detectedLocation);
expect(result).toBe(
'š **Detected location:** Paris, France (approximate)\nš” *For more accurate weather, specify your exact location using city name, address, or coordinates.*'
);
});
});
});