mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
379 lines
13 KiB
Ruby
379 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe Api::V1::LocationsController, type: :request do
|
|
let(:user) { create(:user) }
|
|
let(:api_key) { user.api_key }
|
|
let(:headers) { { 'Authorization' => "Bearer #{api_key}" } }
|
|
|
|
describe 'GET /api/v1/locations' do
|
|
context 'with valid authentication' do
|
|
context 'when coordinates are provided' do
|
|
let(:latitude) { 52.5200 }
|
|
let(:longitude) { 13.4050 }
|
|
let(:mock_search_result) do
|
|
{
|
|
query: nil,
|
|
locations: [
|
|
{
|
|
place_name: 'Kaufland Mitte',
|
|
coordinates: [52.5200, 13.4050],
|
|
address: 'Alexanderplatz 1, Berlin',
|
|
total_visits: 2,
|
|
first_visit: '2024-01-15T09:30:00Z',
|
|
last_visit: '2024-03-20T18:45:00Z',
|
|
visits: [
|
|
{
|
|
timestamp: 1711814700,
|
|
date: '2024-03-20T18:45:00Z',
|
|
coordinates: [52.5201, 13.4051],
|
|
distance_meters: 45.5,
|
|
duration_estimate: '~25m',
|
|
points_count: 8
|
|
}
|
|
]
|
|
}
|
|
],
|
|
total_locations: 1,
|
|
search_metadata: {
|
|
geocoding_provider: 'photon',
|
|
candidates_found: 3,
|
|
search_time_ms: 234
|
|
}
|
|
}
|
|
end
|
|
|
|
before do
|
|
allow_any_instance_of(LocationSearch::PointFinder)
|
|
.to receive(:call).and_return(mock_search_result)
|
|
end
|
|
|
|
it 'returns successful response with search results' do
|
|
get '/api/v1/locations', params: { lat: latitude, lon: longitude }, headers: headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['query']).to be_nil
|
|
expect(json_response['locations']).to be_an(Array)
|
|
expect(json_response['locations'].first['place_name']).to eq('Kaufland Mitte')
|
|
expect(json_response['total_locations']).to eq(1)
|
|
end
|
|
|
|
it 'includes search metadata in response' do
|
|
get '/api/v1/locations', params: { lat: latitude, lon: longitude }, headers: headers
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['search_metadata']).to include(
|
|
'geocoding_provider' => 'photon',
|
|
'candidates_found' => 3
|
|
)
|
|
end
|
|
|
|
it 'passes search parameters to PointFinder service' do
|
|
expect(LocationSearch::PointFinder)
|
|
.to receive(:new)
|
|
.with(user, hash_including(
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
limit: 50,
|
|
date_from: nil,
|
|
date_to: nil,
|
|
radius_override: nil
|
|
))
|
|
.and_return(double(call: mock_search_result))
|
|
|
|
get '/api/v1/locations', params: { lat: latitude, lon: longitude }, headers: headers
|
|
end
|
|
|
|
context 'with additional search parameters' do
|
|
let(:params) do
|
|
{
|
|
lat: latitude,
|
|
lon: longitude,
|
|
limit: 20,
|
|
date_from: '2024-01-01',
|
|
date_to: '2024-03-31',
|
|
radius_override: 200
|
|
}
|
|
end
|
|
|
|
it 'passes all parameters to the service' do
|
|
expect(LocationSearch::PointFinder)
|
|
.to receive(:new)
|
|
.with(user, hash_including(
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
limit: 20,
|
|
date_from: Date.parse('2024-01-01'),
|
|
date_to: Date.parse('2024-03-31'),
|
|
radius_override: 200
|
|
))
|
|
.and_return(double(call: mock_search_result))
|
|
|
|
get '/api/v1/locations', params: params, headers: headers
|
|
end
|
|
end
|
|
|
|
context 'with invalid date parameters' do
|
|
it 'handles invalid date_from gracefully' do
|
|
expect {
|
|
get '/api/v1/locations', params: { lat: latitude, lon: longitude, date_from: 'invalid-date' }, headers: headers
|
|
}.not_to raise_error
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
end
|
|
|
|
it 'handles invalid date_to gracefully' do
|
|
expect {
|
|
get '/api/v1/locations', params: { lat: latitude, lon: longitude, date_to: 'invalid-date' }, headers: headers
|
|
}.not_to raise_error
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when no search results are found' do
|
|
let(:empty_result) do
|
|
{
|
|
query: 'NonexistentPlace',
|
|
locations: [],
|
|
total_locations: 0,
|
|
search_metadata: { geocoding_provider: nil, candidates_found: 0, search_time_ms: 0 }
|
|
}
|
|
end
|
|
|
|
before do
|
|
allow_any_instance_of(LocationSearch::PointFinder)
|
|
.to receive(:call).and_return(empty_result)
|
|
end
|
|
|
|
it 'returns empty results successfully' do
|
|
get '/api/v1/locations', params: { lat: 0.0, lon: 0.0 }, headers: headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['locations']).to be_empty
|
|
expect(json_response['total_locations']).to eq(0)
|
|
end
|
|
end
|
|
|
|
context 'when coordinates are missing' do
|
|
it 'returns bad request error' do
|
|
get '/api/v1/locations', headers: headers
|
|
|
|
expect(response).to have_http_status(:bad_request)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['error']).to eq('Coordinates (lat, lon) are required')
|
|
end
|
|
end
|
|
|
|
context 'when only latitude is provided' do
|
|
it 'returns bad request error' do
|
|
get '/api/v1/locations', params: { lat: 52.5200 }, headers: headers
|
|
|
|
expect(response).to have_http_status(:bad_request)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['error']).to eq('Coordinates (lat, lon) are required')
|
|
end
|
|
end
|
|
|
|
context 'when coordinates are invalid' do
|
|
it 'returns bad request error for invalid latitude' do
|
|
get '/api/v1/locations', params: { lat: 91, lon: 0 }, headers: headers
|
|
|
|
expect(response).to have_http_status(:bad_request)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['error']).to eq('Invalid coordinates: latitude must be between -90 and 90, longitude between -180 and 180')
|
|
end
|
|
|
|
it 'returns bad request error for invalid longitude' do
|
|
get '/api/v1/locations', params: { lat: 0, lon: 181 }, headers: headers
|
|
|
|
expect(response).to have_http_status(:bad_request)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['error']).to eq('Invalid coordinates: latitude must be between -90 and 90, longitude between -180 and 180')
|
|
end
|
|
end
|
|
|
|
context 'when service raises an error' do
|
|
before do
|
|
allow_any_instance_of(LocationSearch::PointFinder)
|
|
.to receive(:call).and_raise(StandardError.new('Service error'))
|
|
end
|
|
|
|
it 'returns internal server error' do
|
|
get '/api/v1/locations', params: { lat: 52.5200, lon: 13.4050 }, headers: headers
|
|
|
|
expect(response).to have_http_status(:internal_server_error)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['error']).to eq('Search failed. Please try again.')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'without authentication' do
|
|
it 'returns unauthorized error' do
|
|
get '/api/v1/locations', params: { lat: 52.5200, lon: 13.4050 }
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'with invalid API key' do
|
|
let(:invalid_headers) { { 'Authorization' => 'Bearer invalid_key' } }
|
|
|
|
it 'returns unauthorized error' do
|
|
get '/api/v1/locations', params: { lat: 52.5200, lon: 13.4050 }, headers: invalid_headers
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'with user data isolation' do
|
|
let(:user1) { create(:user) }
|
|
let(:user2) { create(:user) }
|
|
let(:user1_headers) { { 'Authorization' => "Bearer #{user1.api_key}" } }
|
|
|
|
before do
|
|
# Create points for both users
|
|
create(:point, user: user1, latitude: 52.5200, longitude: 13.4050)
|
|
create(:point, user: user2, latitude: 52.5200, longitude: 13.4050)
|
|
|
|
# Mock service to verify user isolation
|
|
allow(LocationSearch::PointFinder).to receive(:new) do |user, _params|
|
|
expect(user).to eq(user1) # Should only be called with user1
|
|
double(call: { query: nil, locations: [], total_locations: 0, search_metadata: {} })
|
|
end
|
|
end
|
|
|
|
it 'only searches within the authenticated user data' do
|
|
get '/api/v1/locations', params: { lat: 52.5200, lon: 13.4050 }, headers: user1_headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/locations/suggestions' do
|
|
context 'with valid authentication' do
|
|
let(:mock_suggestions) do
|
|
[
|
|
{
|
|
lat: 52.5200,
|
|
lon: 13.4050,
|
|
name: 'Kaufland Mitte',
|
|
address: 'Alexanderplatz 1, Berlin',
|
|
type: 'shop'
|
|
},
|
|
{
|
|
lat: 52.5100,
|
|
lon: 13.4000,
|
|
name: 'Kaufland Friedrichshain',
|
|
address: 'Warschauer Str. 80, Berlin',
|
|
type: 'shop'
|
|
}
|
|
]
|
|
end
|
|
|
|
before do
|
|
allow_any_instance_of(LocationSearch::GeocodingService)
|
|
.to receive(:search).and_return(mock_suggestions)
|
|
end
|
|
|
|
context 'with valid search query' do
|
|
it 'returns formatted suggestions' do
|
|
get '/api/v1/locations/suggestions', params: { q: 'Kaufland' }, headers: headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['suggestions']).to be_an(Array)
|
|
expect(json_response['suggestions'].length).to eq(2)
|
|
|
|
first_suggestion = json_response['suggestions'].first
|
|
expect(first_suggestion).to include(
|
|
'name' => 'Kaufland Mitte',
|
|
'address' => 'Alexanderplatz 1, Berlin',
|
|
'coordinates' => [52.5200, 13.4050],
|
|
'type' => 'shop'
|
|
)
|
|
end
|
|
|
|
it 'limits suggestions to 10 results' do
|
|
large_suggestions = Array.new(10) do |i|
|
|
{
|
|
lat: 52.5000 + i * 0.001,
|
|
lon: 13.4000 + i * 0.001,
|
|
name: "Location #{i}",
|
|
address: "Address #{i}",
|
|
type: 'place'
|
|
}
|
|
end
|
|
|
|
allow_any_instance_of(LocationSearch::GeocodingService)
|
|
.to receive(:search).and_return(large_suggestions)
|
|
|
|
get '/api/v1/locations/suggestions', params: { q: 'test' }, headers: headers
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['suggestions'].length).to eq(10)
|
|
end
|
|
end
|
|
|
|
context 'with short search query' do
|
|
it 'returns empty suggestions for queries shorter than 2 characters' do
|
|
get '/api/v1/locations/suggestions', params: { q: 'a' }, headers: headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['suggestions']).to be_empty
|
|
end
|
|
end
|
|
|
|
context 'with blank query' do
|
|
it 'returns empty suggestions' do
|
|
get '/api/v1/locations/suggestions', params: { q: '' }, headers: headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['suggestions']).to be_empty
|
|
end
|
|
end
|
|
|
|
context 'when geocoding service raises an error' do
|
|
before do
|
|
allow_any_instance_of(LocationSearch::GeocodingService)
|
|
.to receive(:search).and_raise(StandardError.new('Geocoding error'))
|
|
end
|
|
|
|
it 'returns empty suggestions gracefully' do
|
|
get '/api/v1/locations/suggestions', params: { q: 'test' }, headers: headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
json_response = JSON.parse(response.body)
|
|
expect(json_response['suggestions']).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'without authentication' do
|
|
it 'returns unauthorized error' do
|
|
get '/api/v1/locations/suggestions', params: { q: 'test' }
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
end
|
|
end
|