dawarich/spec/services/visits/place_finder_spec.rb
2025-11-22 13:28:38 +01:00

277 lines
8.7 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Visits::PlaceFinder do
let(:user) { create(:user) }
let(:latitude) { 40.7128 }
let(:longitude) { -74.0060 }
subject { described_class.new(user) }
describe '#find_or_create_place' do
let(:visit_data) do
{
center_lat: latitude,
center_lon: longitude,
suggested_name: 'Test Place',
points: []
}
end
context 'when an existing place is found' do
let!(:existing_place) { create(:place, latitude: latitude, longitude: longitude) }
it 'returns the existing place as main_place' do
result = subject.find_or_create_place(visit_data)
expect(result).to be_a(Hash)
expect(result[:main_place]).to eq(existing_place)
end
it 'includes suggested places in the result' do
result = subject.find_or_create_place(visit_data)
expect(result[:suggested_places]).to respond_to(:each)
expect(result[:suggested_places]).to include(existing_place)
end
it 'finds an existing place by name within search radius' do
similar_named_place = create(:place,
name: 'Test Place',
latitude: latitude + 0.0001,
longitude: longitude + 0.0001)
allow(subject).to receive(:find_existing_place).and_return(similar_named_place)
modified_visit_data = visit_data.merge(
center_lat: latitude + 0.0002,
center_lon: longitude + 0.0002
)
result = subject.find_or_create_place(modified_visit_data)
expect(result[:main_place]).to eq(similar_named_place)
end
end
context 'with places from points data' do
let(:point_with_geodata) do
build_stubbed(:point,
lonlat: "POINT(#{longitude} #{latitude})",
geodata: {
'properties' => {
'name' => 'POI from Point',
'city' => 'New York',
'country' => 'USA'
}
})
end
let(:visit_data_with_points) do
visit_data.merge(points: [point_with_geodata])
end
before do
allow(Geocoder).to receive(:search).and_return([])
allow(subject).to receive(:reverse_geocoded_places).and_return([])
end
it 'extracts and creates places from point geodata' do
allow(subject).to receive(:create_place_from_point).and_call_original
expect do
result = subject.find_or_create_place(visit_data_with_points)
expect(result[:main_place].name).to include('POI from Point')
end.to change(Place, :count).by(1)
expect(subject).to have_received(:create_place_from_point)
end
end
context 'when no existing place is found' do
let(:geocoder_result) do
double(
data: {
'properties' => {
'name' => 'Test Location',
'street' => 'Test Street',
'city' => 'Test City',
'country' => 'Test Country'
}
},
latitude: latitude,
longitude: longitude
)
end
let(:other_geocoder_result) do
double(
data: {
'properties' => {
'name' => 'Other Location',
'street' => 'Other Street',
'city' => 'Test City',
'country' => 'Test Country'
}
},
latitude: latitude + 0.001,
longitude: longitude + 0.001
)
end
before do
allow(Geocoder).to receive(:search).and_return([geocoder_result, other_geocoder_result])
end
it 'creates a new place with geocoded data' do
expect do
result = subject.find_or_create_place(visit_data)
expect(result[:main_place].name).to include('Test Location')
end.to change(Place, :count).by(2)
place = Place.find_by_name('Test Location, Test Street, Test City')
expect(place.city).to eq('Test City')
expect(place.country).to eq('Test Country')
expect(place.source).to eq('photon')
end
it 'returns both main place and suggested places' do
result = subject.find_or_create_place(visit_data)
expect(result[:main_place].name).to include('Test Location')
expect(result[:suggested_places].length).to eq(2)
expect(result[:suggested_places].map(&:name)).to include(
'Test Location, Test Street, Test City',
'Other Location, Other Street, Test City'
)
end
context 'when geocoding returns no results' do
before do
allow(Geocoder).to receive(:search).and_return([])
end
it 'creates a place with the suggested name' do
expect do
result = subject.find_or_create_place(visit_data)
expect(result[:main_place].name).to eq('Test Place')
end.to change(Place, :count).by(1)
place = Place.last
expect(place.name).to eq('Test Place')
expect(place.source).to eq('manual')
end
it 'returns the created place as both main and the only suggested place' do
result = subject.find_or_create_place(visit_data)
expect(result[:main_place].name).to eq('Test Place')
expect(result[:suggested_places]).to eq([result[:main_place]])
end
it 'falls back to default name when suggested name is missing' do
visit_data_without_name = visit_data.merge(suggested_name: nil)
result = subject.find_or_create_place(visit_data_without_name)
expect(result[:main_place].name).to eq(Place::DEFAULT_NAME)
end
end
end
context 'with multiple potential places' do
let!(:place1) { create(:place, name: 'Place 1', latitude: latitude, longitude: longitude) }
let!(:place2) { create(:place, name: 'Place 2', latitude: latitude + 0.0005, longitude: longitude + 0.0005) }
let!(:place3) { create(:place, name: 'Place 3', latitude: latitude + 0.001, longitude: longitude + 0.001) }
it 'selects the closest place as main_place' do
result = subject.find_or_create_place(visit_data)
expect(result[:main_place]).to eq(place1)
end
it 'includes nearby places as suggested_places' do
result = subject.find_or_create_place(visit_data)
expect(result[:suggested_places]).to include(place1, place2)
# place3 might be outside the search radius depending on the constants defined
end
it 'may include places with the same name' do
dup_place = create(:place, name: 'Place 1', latitude: latitude + 0.0002, longitude: longitude + 0.0002)
allow(subject).to receive(:place_name_exists?).and_return(false)
result = subject.find_or_create_place(visit_data)
names = result[:suggested_places].map(&:name)
expect(names.count('Place 1')).to be >= 1
end
end
context 'with API place creation failures' do
let(:invalid_geocoder_result) do
double(
data: {
'properties' => {
# Missing required fields
}
},
latitude: latitude,
longitude: longitude
)
end
before do
allow(Geocoder).to receive(:search).and_return([invalid_geocoder_result])
end
it 'gracefully handles errors in place creation' do
allow(subject).to receive(:create_place_from_api_result).and_call_original
result = subject.find_or_create_place(visit_data)
# Should create the default place
expect(result[:main_place].name).to eq('Test Place')
expect(result[:main_place].source).to eq('manual')
end
end
end
describe 'private methods' do
context '#build_place_name' do
it 'combines name components correctly' do
properties = {
'name' => 'Coffee Shop',
'street' => 'Main St',
'housenumber' => '123',
'city' => 'New York'
}
name = subject.send(:build_place_name, properties)
expect(name).to eq('Coffee Shop, Main St, 123, New York')
end
it 'removes duplicate components' do
properties = {
'name' => 'Coffee Shop',
'street' => 'Coffee Shop', # Duplicate of name
'city' => 'New York'
}
name = subject.send(:build_place_name, properties)
expect(name).to eq('Coffee Shop, New York')
end
it 'returns default name when no components are available' do
properties = { 'other' => 'irrelevant' }
name = subject.send(:build_place_name, properties)
expect(name).to eq(Place::DEFAULT_NAME)
end
end
end
end