backend unit tests
This commit is contained in:
940
backend/tests/unit/services/googleMapsService.test.js
Normal file
940
backend/tests/unit/services/googleMapsService.test.js
Normal file
@@ -0,0 +1,940 @@
|
||||
// Mock the Google Maps client
|
||||
const mockPlaceAutocomplete = jest.fn();
|
||||
const mockPlaceDetails = jest.fn();
|
||||
const mockGeocode = jest.fn();
|
||||
|
||||
jest.mock('@googlemaps/google-maps-services-js', () => ({
|
||||
Client: jest.fn().mockImplementation(() => ({
|
||||
placeAutocomplete: mockPlaceAutocomplete,
|
||||
placeDetails: mockPlaceDetails,
|
||||
geocode: mockGeocode
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('GoogleMapsService', () => {
|
||||
let service;
|
||||
let consoleSpy, consoleErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set up console spies
|
||||
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
// Reset environment
|
||||
delete process.env.GOOGLE_MAPS_API_KEY;
|
||||
|
||||
// Clear module cache to get fresh instance
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('Constructor', () => {
|
||||
it('should initialize with API key and log success', () => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
|
||||
service = require('../../../services/googleMapsService');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('✅ Google Maps service initialized');
|
||||
expect(service.isConfigured()).toBe(true);
|
||||
});
|
||||
|
||||
it('should log error when API key is not configured', () => {
|
||||
delete process.env.GOOGLE_MAPS_API_KEY;
|
||||
|
||||
service = require('../../../services/googleMapsService');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('❌ Google Maps API key not configured in environment variables');
|
||||
expect(service.isConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize Google Maps Client', () => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
const { Client } = require('@googlemaps/google-maps-services-js');
|
||||
|
||||
service = require('../../../services/googleMapsService');
|
||||
|
||||
expect(Client).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlacesAutocomplete', () => {
|
||||
beforeEach(() => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
service = require('../../../services/googleMapsService');
|
||||
});
|
||||
|
||||
describe('Input validation', () => {
|
||||
it('should throw error when API key is not configured', async () => {
|
||||
service.apiKey = null;
|
||||
|
||||
await expect(service.getPlacesAutocomplete('test')).rejects.toThrow('Google Maps API key not configured');
|
||||
});
|
||||
|
||||
it('should return empty predictions for empty input', async () => {
|
||||
const result = await service.getPlacesAutocomplete('');
|
||||
|
||||
expect(result).toEqual({ predictions: [] });
|
||||
expect(mockPlaceAutocomplete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty predictions for input less than 2 characters', async () => {
|
||||
const result = await service.getPlacesAutocomplete('a');
|
||||
|
||||
expect(result).toEqual({ predictions: [] });
|
||||
expect(mockPlaceAutocomplete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trim input and proceed with valid input', async () => {
|
||||
mockPlaceAutocomplete.mockResolvedValue({
|
||||
data: {
|
||||
status: 'OK',
|
||||
predictions: []
|
||||
}
|
||||
});
|
||||
|
||||
await service.getPlacesAutocomplete(' test ');
|
||||
|
||||
expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
input: 'test'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parameters handling', () => {
|
||||
beforeEach(() => {
|
||||
mockPlaceAutocomplete.mockResolvedValue({
|
||||
data: {
|
||||
status: 'OK',
|
||||
predictions: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default parameters', async () => {
|
||||
await service.getPlacesAutocomplete('test input');
|
||||
|
||||
expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
|
||||
params: {
|
||||
key: 'test-api-key',
|
||||
input: 'test input',
|
||||
types: 'address',
|
||||
language: 'en'
|
||||
},
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept custom options', async () => {
|
||||
const options = {
|
||||
types: 'establishment',
|
||||
language: 'fr'
|
||||
};
|
||||
|
||||
await service.getPlacesAutocomplete('test input', options);
|
||||
|
||||
expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
|
||||
params: {
|
||||
key: 'test-api-key',
|
||||
input: 'test input',
|
||||
types: 'establishment',
|
||||
language: 'fr'
|
||||
},
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should include session token when provided', async () => {
|
||||
const options = {
|
||||
sessionToken: 'session-123'
|
||||
};
|
||||
|
||||
await service.getPlacesAutocomplete('test input', options);
|
||||
|
||||
expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
sessiontoken: 'session-123'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle component restrictions', async () => {
|
||||
const options = {
|
||||
componentRestrictions: {
|
||||
country: 'us',
|
||||
administrative_area: 'CA'
|
||||
}
|
||||
};
|
||||
|
||||
await service.getPlacesAutocomplete('test input', options);
|
||||
|
||||
expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
components: 'country:us|administrative_area:CA'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge additional options', async () => {
|
||||
const options = {
|
||||
radius: 1000,
|
||||
location: '40.7128,-74.0060'
|
||||
};
|
||||
|
||||
await service.getPlacesAutocomplete('test input', options);
|
||||
|
||||
expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
radius: 1000,
|
||||
location: '40.7128,-74.0060'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful responses', () => {
|
||||
it('should return formatted predictions on success', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
predictions: [
|
||||
{
|
||||
place_id: 'ChIJ123',
|
||||
description: 'Test Location, City, State',
|
||||
types: ['establishment'],
|
||||
structured_formatting: {
|
||||
main_text: 'Test Location',
|
||||
secondary_text: 'City, State'
|
||||
}
|
||||
},
|
||||
{
|
||||
place_id: 'ChIJ456',
|
||||
description: 'Another Place',
|
||||
types: ['locality'],
|
||||
structured_formatting: {
|
||||
main_text: 'Another Place'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceAutocomplete.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlacesAutocomplete('test input');
|
||||
|
||||
expect(result).toEqual({
|
||||
predictions: [
|
||||
{
|
||||
placeId: 'ChIJ123',
|
||||
description: 'Test Location, City, State',
|
||||
types: ['establishment'],
|
||||
mainText: 'Test Location',
|
||||
secondaryText: 'City, State'
|
||||
},
|
||||
{
|
||||
placeId: 'ChIJ456',
|
||||
description: 'Another Place',
|
||||
types: ['locality'],
|
||||
mainText: 'Another Place',
|
||||
secondaryText: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle predictions without secondary text', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
predictions: [
|
||||
{
|
||||
place_id: 'ChIJ123',
|
||||
description: 'Test Location',
|
||||
types: ['establishment'],
|
||||
structured_formatting: {
|
||||
main_text: 'Test Location'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceAutocomplete.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlacesAutocomplete('test input');
|
||||
|
||||
expect(result.predictions[0].secondaryText).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error responses', () => {
|
||||
it('should handle API error responses', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'ZERO_RESULTS',
|
||||
error_message: 'No results found'
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceAutocomplete.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlacesAutocomplete('test input');
|
||||
|
||||
expect(result).toEqual({
|
||||
predictions: [],
|
||||
error: 'No results found for this query',
|
||||
status: 'ZERO_RESULTS'
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Places Autocomplete API error:',
|
||||
'ZERO_RESULTS',
|
||||
'No results found'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle unknown error status', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'UNKNOWN_STATUS'
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceAutocomplete.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlacesAutocomplete('test input');
|
||||
|
||||
expect(result.error).toBe('Google Maps API error: UNKNOWN_STATUS');
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockPlaceAutocomplete.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(service.getPlacesAutocomplete('test input')).rejects.toThrow('Failed to fetch place predictions');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Places Autocomplete service error:', 'Network error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlaceDetails', () => {
|
||||
beforeEach(() => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
service = require('../../../services/googleMapsService');
|
||||
});
|
||||
|
||||
describe('Input validation', () => {
|
||||
it('should throw error when API key is not configured', async () => {
|
||||
service.apiKey = null;
|
||||
|
||||
await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('Google Maps API key not configured');
|
||||
});
|
||||
|
||||
it('should throw error when placeId is not provided', async () => {
|
||||
await expect(service.getPlaceDetails()).rejects.toThrow('Place ID is required');
|
||||
await expect(service.getPlaceDetails('')).rejects.toThrow('Place ID is required');
|
||||
await expect(service.getPlaceDetails(null)).rejects.toThrow('Place ID is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parameters handling', () => {
|
||||
beforeEach(() => {
|
||||
mockPlaceDetails.mockResolvedValue({
|
||||
data: {
|
||||
status: 'OK',
|
||||
result: {
|
||||
place_id: 'ChIJ123',
|
||||
formatted_address: 'Test Address',
|
||||
address_components: [],
|
||||
geometry: {
|
||||
location: { lat: 40.7128, lng: -74.0060 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default parameters', async () => {
|
||||
await service.getPlaceDetails('ChIJ123');
|
||||
|
||||
expect(mockPlaceDetails).toHaveBeenCalledWith({
|
||||
params: {
|
||||
key: 'test-api-key',
|
||||
place_id: 'ChIJ123',
|
||||
fields: [
|
||||
'address_components',
|
||||
'formatted_address',
|
||||
'geometry',
|
||||
'place_id'
|
||||
],
|
||||
language: 'en'
|
||||
},
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept custom language', async () => {
|
||||
await service.getPlaceDetails('ChIJ123', { language: 'fr' });
|
||||
|
||||
expect(mockPlaceDetails).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
language: 'fr'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should include session token when provided', async () => {
|
||||
await service.getPlaceDetails('ChIJ123', { sessionToken: 'session-123' });
|
||||
|
||||
expect(mockPlaceDetails).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
sessiontoken: 'session-123'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful responses', () => {
|
||||
it('should return formatted place details', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
result: {
|
||||
place_id: 'ChIJ123',
|
||||
formatted_address: '123 Test St, Test City, TC 12345, USA',
|
||||
address_components: [
|
||||
{
|
||||
long_name: '123',
|
||||
short_name: '123',
|
||||
types: ['street_number']
|
||||
},
|
||||
{
|
||||
long_name: 'Test Street',
|
||||
short_name: 'Test St',
|
||||
types: ['route']
|
||||
},
|
||||
{
|
||||
long_name: 'Test City',
|
||||
short_name: 'Test City',
|
||||
types: ['locality', 'political']
|
||||
},
|
||||
{
|
||||
long_name: 'Test State',
|
||||
short_name: 'TS',
|
||||
types: ['administrative_area_level_1', 'political']
|
||||
},
|
||||
{
|
||||
long_name: '12345',
|
||||
short_name: '12345',
|
||||
types: ['postal_code']
|
||||
},
|
||||
{
|
||||
long_name: 'United States',
|
||||
short_name: 'US',
|
||||
types: ['country', 'political']
|
||||
}
|
||||
],
|
||||
geometry: {
|
||||
location: { lat: 40.7128, lng: -74.0060 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceDetails.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlaceDetails('ChIJ123');
|
||||
|
||||
expect(result).toEqual({
|
||||
placeId: 'ChIJ123',
|
||||
formattedAddress: '123 Test St, Test City, TC 12345, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '123',
|
||||
route: 'Test Street',
|
||||
locality: 'Test City',
|
||||
administrativeAreaLevel1: 'TS',
|
||||
administrativeAreaLevel1Long: 'Test State',
|
||||
postalCode: '12345',
|
||||
country: 'US'
|
||||
},
|
||||
geometry: {
|
||||
latitude: 40.7128,
|
||||
longitude: -74.0060
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle place details without address components', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
result: {
|
||||
place_id: 'ChIJ123',
|
||||
formatted_address: 'Test Address',
|
||||
geometry: {
|
||||
location: { lat: 40.7128, lng: -74.0060 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceDetails.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlaceDetails('ChIJ123');
|
||||
|
||||
expect(result.addressComponents).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle place details without geometry', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
result: {
|
||||
place_id: 'ChIJ123',
|
||||
formatted_address: 'Test Address'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceDetails.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlaceDetails('ChIJ123');
|
||||
|
||||
expect(result.geometry).toEqual({
|
||||
latitude: 0,
|
||||
longitude: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle partial geometry data', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
result: {
|
||||
place_id: 'ChIJ123',
|
||||
formatted_address: 'Test Address',
|
||||
geometry: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceDetails.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPlaceDetails('ChIJ123');
|
||||
|
||||
expect(result.geometry).toEqual({
|
||||
latitude: 0,
|
||||
longitude: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error responses', () => {
|
||||
it('should handle API error responses', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'NOT_FOUND',
|
||||
error_message: 'Place not found'
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceDetails.mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('The specified place was not found');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Place Details API error:',
|
||||
'NOT_FOUND',
|
||||
'Place not found'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle response without result', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK'
|
||||
}
|
||||
};
|
||||
|
||||
mockPlaceDetails.mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('Google Maps API error: OK');
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
const originalError = new Error('Network error');
|
||||
mockPlaceDetails.mockRejectedValue(originalError);
|
||||
|
||||
await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow(originalError);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Place Details service error:', 'Network error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('geocodeAddress', () => {
|
||||
beforeEach(() => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
service = require('../../../services/googleMapsService');
|
||||
});
|
||||
|
||||
describe('Input validation', () => {
|
||||
it('should throw error when API key is not configured', async () => {
|
||||
service.apiKey = null;
|
||||
|
||||
await expect(service.geocodeAddress('123 Test St')).rejects.toThrow('Google Maps API key not configured');
|
||||
});
|
||||
|
||||
it('should throw error when address is not provided', async () => {
|
||||
await expect(service.geocodeAddress()).rejects.toThrow('Address is required for geocoding');
|
||||
await expect(service.geocodeAddress('')).rejects.toThrow('Address is required for geocoding');
|
||||
await expect(service.geocodeAddress(' ')).rejects.toThrow('Address is required for geocoding');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parameters handling', () => {
|
||||
beforeEach(() => {
|
||||
mockGeocode.mockResolvedValue({
|
||||
data: {
|
||||
status: 'OK',
|
||||
results: [
|
||||
{
|
||||
formatted_address: 'Test Address',
|
||||
place_id: 'ChIJ123',
|
||||
geometry: {
|
||||
location: { lat: 40.7128, lng: -74.0060 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default parameters', async () => {
|
||||
await service.geocodeAddress('123 Test St');
|
||||
|
||||
expect(mockGeocode).toHaveBeenCalledWith({
|
||||
params: {
|
||||
key: 'test-api-key',
|
||||
address: '123 Test St',
|
||||
language: 'en'
|
||||
},
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should trim address input', async () => {
|
||||
await service.geocodeAddress(' 123 Test St ');
|
||||
|
||||
expect(mockGeocode).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
address: '123 Test St'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept custom language', async () => {
|
||||
await service.geocodeAddress('123 Test St', { language: 'fr' });
|
||||
|
||||
expect(mockGeocode).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
language: 'fr'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle component restrictions', async () => {
|
||||
const options = {
|
||||
componentRestrictions: {
|
||||
country: 'us',
|
||||
administrative_area: 'CA'
|
||||
}
|
||||
};
|
||||
|
||||
await service.geocodeAddress('123 Test St', options);
|
||||
|
||||
expect(mockGeocode).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
components: 'country:us|administrative_area:CA'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle bounds parameter', async () => {
|
||||
const options = {
|
||||
bounds: '40.7,-74.1|40.8,-73.9'
|
||||
};
|
||||
|
||||
await service.geocodeAddress('123 Test St', options);
|
||||
|
||||
expect(mockGeocode).toHaveBeenCalledWith({
|
||||
params: expect.objectContaining({
|
||||
bounds: '40.7,-74.1|40.8,-73.9'
|
||||
}),
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful responses', () => {
|
||||
it('should return geocoded location', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
results: [
|
||||
{
|
||||
formatted_address: '123 Test St, Test City, TC 12345, USA',
|
||||
place_id: 'ChIJ123',
|
||||
geometry: {
|
||||
location: { lat: 40.7128, lng: -74.0060 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
mockGeocode.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.geocodeAddress('123 Test St');
|
||||
|
||||
expect(result).toEqual({
|
||||
latitude: 40.7128,
|
||||
longitude: -74.0060,
|
||||
formattedAddress: '123 Test St, Test City, TC 12345, USA',
|
||||
placeId: 'ChIJ123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return first result when multiple results', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
results: [
|
||||
{
|
||||
formatted_address: 'First Result',
|
||||
place_id: 'ChIJ123',
|
||||
geometry: { location: { lat: 40.7128, lng: -74.0060 } }
|
||||
},
|
||||
{
|
||||
formatted_address: 'Second Result',
|
||||
place_id: 'ChIJ456',
|
||||
geometry: { location: { lat: 40.7129, lng: -74.0061 } }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
mockGeocode.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.geocodeAddress('123 Test St');
|
||||
|
||||
expect(result.formattedAddress).toBe('First Result');
|
||||
expect(result.placeId).toBe('ChIJ123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error responses', () => {
|
||||
it('should handle API error responses', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'ZERO_RESULTS',
|
||||
error_message: 'No results found'
|
||||
}
|
||||
};
|
||||
|
||||
mockGeocode.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.geocodeAddress('123 Test St');
|
||||
|
||||
expect(result).toEqual({
|
||||
error: 'No results found for this query',
|
||||
status: 'ZERO_RESULTS'
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Geocoding API error:',
|
||||
'ZERO_RESULTS',
|
||||
'No results found'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty results array', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'OK',
|
||||
results: []
|
||||
}
|
||||
};
|
||||
|
||||
mockGeocode.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.geocodeAddress('123 Test St');
|
||||
|
||||
expect(result.error).toBe('Google Maps API error: OK');
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockGeocode.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(service.geocodeAddress('123 Test St')).rejects.toThrow('Failed to geocode address');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Geocoding service error:', 'Network error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getErrorMessage', () => {
|
||||
beforeEach(() => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
service = require('../../../services/googleMapsService');
|
||||
});
|
||||
|
||||
it('should return correct error messages for known status codes', () => {
|
||||
expect(service.getErrorMessage('ZERO_RESULTS')).toBe('No results found for this query');
|
||||
expect(service.getErrorMessage('OVER_QUERY_LIMIT')).toBe('API quota exceeded. Please try again later');
|
||||
expect(service.getErrorMessage('REQUEST_DENIED')).toBe('API request denied. Check API key configuration');
|
||||
expect(service.getErrorMessage('INVALID_REQUEST')).toBe('Invalid request parameters');
|
||||
expect(service.getErrorMessage('UNKNOWN_ERROR')).toBe('Server error. Please try again');
|
||||
expect(service.getErrorMessage('NOT_FOUND')).toBe('The specified place was not found');
|
||||
});
|
||||
|
||||
it('should return generic error message for unknown status codes', () => {
|
||||
expect(service.getErrorMessage('UNKNOWN_STATUS')).toBe('Google Maps API error: UNKNOWN_STATUS');
|
||||
expect(service.getErrorMessage('CUSTOM_ERROR')).toBe('Google Maps API error: CUSTOM_ERROR');
|
||||
});
|
||||
|
||||
it('should handle null/undefined status', () => {
|
||||
expect(service.getErrorMessage(null)).toBe('Google Maps API error: null');
|
||||
expect(service.getErrorMessage(undefined)).toBe('Google Maps API error: undefined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConfigured', () => {
|
||||
it('should return true when API key is configured', () => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
service = require('../../../services/googleMapsService');
|
||||
|
||||
expect(service.isConfigured()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when API key is not configured', () => {
|
||||
delete process.env.GOOGLE_MAPS_API_KEY;
|
||||
service = require('../../../services/googleMapsService');
|
||||
|
||||
expect(service.isConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when API key is empty string', () => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = '';
|
||||
service = require('../../../services/googleMapsService');
|
||||
|
||||
expect(service.isConfigured()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Singleton pattern', () => {
|
||||
it('should return the same instance on multiple requires', () => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
|
||||
const service1 = require('../../../services/googleMapsService');
|
||||
const service2 = require('../../../services/googleMapsService');
|
||||
|
||||
expect(service1).toBe(service2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration scenarios', () => {
|
||||
beforeEach(() => {
|
||||
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
|
||||
service = require('../../../services/googleMapsService');
|
||||
});
|
||||
|
||||
it('should handle typical place search workflow', async () => {
|
||||
// Mock autocomplete response
|
||||
mockPlaceAutocomplete.mockResolvedValue({
|
||||
data: {
|
||||
status: 'OK',
|
||||
predictions: [
|
||||
{
|
||||
place_id: 'ChIJ123',
|
||||
description: 'Test Location',
|
||||
types: ['establishment'],
|
||||
structured_formatting: {
|
||||
main_text: 'Test Location',
|
||||
secondary_text: 'City, State'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Mock place details response
|
||||
mockPlaceDetails.mockResolvedValue({
|
||||
data: {
|
||||
status: 'OK',
|
||||
result: {
|
||||
place_id: 'ChIJ123',
|
||||
formatted_address: 'Test Location, City, State',
|
||||
address_components: [],
|
||||
geometry: {
|
||||
location: { lat: 40.7128, lng: -74.0060 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 1: Get autocomplete predictions
|
||||
const autocompleteResult = await service.getPlacesAutocomplete('test loc');
|
||||
expect(autocompleteResult.predictions).toHaveLength(1);
|
||||
|
||||
// Step 2: Get detailed place information
|
||||
const placeId = autocompleteResult.predictions[0].placeId;
|
||||
const detailsResult = await service.getPlaceDetails(placeId);
|
||||
|
||||
expect(detailsResult.placeId).toBe('ChIJ123');
|
||||
expect(detailsResult.geometry.latitude).toBe(40.7128);
|
||||
expect(detailsResult.geometry.longitude).toBe(-74.0060);
|
||||
});
|
||||
|
||||
it('should handle geocoding workflow', async () => {
|
||||
mockGeocode.mockResolvedValue({
|
||||
data: {
|
||||
status: 'OK',
|
||||
results: [
|
||||
{
|
||||
formatted_address: '123 Test St, Test City, TC 12345, USA',
|
||||
place_id: 'ChIJ123',
|
||||
geometry: {
|
||||
location: { lat: 40.7128, lng: -74.0060 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = await service.geocodeAddress('123 Test St, Test City, TC');
|
||||
|
||||
expect(result.latitude).toBe(40.7128);
|
||||
expect(result.longitude).toBe(-74.0060);
|
||||
expect(result.formattedAddress).toBe('123 Test St, Test City, TC 12345, USA');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user