mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
377 lines
No EOL
12 KiB
Ruby
377 lines
No EOL
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe Maps::HexagonRequestHandler do
|
|
describe '.call' do
|
|
subject(:handle_request) do
|
|
described_class.call(
|
|
params: params,
|
|
current_api_user: current_api_user
|
|
)
|
|
end
|
|
|
|
let(:user) { create(:user) }
|
|
let(:current_api_user) { user }
|
|
|
|
before do
|
|
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
|
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
|
end
|
|
|
|
context 'with authenticated user and bounding box params' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000,
|
|
start_date: '2024-06-01T00:00:00Z',
|
|
end_date: '2024-06-30T23:59:59Z'
|
|
})
|
|
end
|
|
|
|
before do
|
|
# Create test points within the date range and bounding box
|
|
10.times do |i|
|
|
create(:point,
|
|
user:,
|
|
latitude: 40.7 + (i * 0.001),
|
|
longitude: -74.0 + (i * 0.001),
|
|
timestamp: Time.new(2024, 6, 15, 12, i).to_i)
|
|
end
|
|
end
|
|
|
|
it 'returns on-the-fly hexagon calculation' do
|
|
result = handle_request
|
|
|
|
expect(result).to be_a(Hash)
|
|
expect(result['type']).to eq('FeatureCollection')
|
|
expect(result['features']).to be_an(Array)
|
|
expect(result['metadata']).to be_present
|
|
end
|
|
end
|
|
|
|
context 'with public sharing UUID and pre-calculated centers' do
|
|
let(:pre_calculated_centers) do
|
|
[
|
|
[-74.0, 40.7, 1_717_200_000, 1_717_203_600],
|
|
[-74.01, 40.71, 1_717_210_000, 1_717_213_600]
|
|
]
|
|
end
|
|
let(:stat) do
|
|
create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6,
|
|
hexagon_centers: pre_calculated_centers)
|
|
end
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
uuid: stat.sharing_uuid,
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000
|
|
})
|
|
end
|
|
let(:current_api_user) { nil }
|
|
|
|
it 'returns pre-calculated hexagon data' do
|
|
result = handle_request
|
|
|
|
expect(result['type']).to eq('FeatureCollection')
|
|
expect(result['features'].length).to eq(2)
|
|
expect(result['metadata']['pre_calculated']).to be true
|
|
expect(result['metadata']['user_id']).to eq(user.id)
|
|
end
|
|
end
|
|
|
|
context 'with public sharing UUID but no pre-calculated centers' do
|
|
let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) }
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
uuid: stat.sharing_uuid,
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000
|
|
})
|
|
end
|
|
let(:current_api_user) { nil }
|
|
|
|
before do
|
|
# Create test points for the stat's month
|
|
5.times do |i|
|
|
create(:point,
|
|
user:,
|
|
latitude: 40.7 + (i * 0.001),
|
|
longitude: -74.0 + (i * 0.001),
|
|
timestamp: Time.new(2024, 6, 15, 12, i).to_i)
|
|
end
|
|
end
|
|
|
|
it 'falls back to on-the-fly calculation' do
|
|
result = handle_request
|
|
|
|
expect(result['type']).to eq('FeatureCollection')
|
|
expect(result['features']).to be_an(Array)
|
|
expect(result['metadata']).to be_present
|
|
expect(result['metadata']['pre_calculated']).to be_falsy
|
|
end
|
|
end
|
|
|
|
context 'with legacy area_too_large that can be recalculated' do
|
|
let(:stat) do
|
|
create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6,
|
|
hexagon_centers: { 'area_too_large' => true })
|
|
end
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
uuid: stat.sharing_uuid,
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000
|
|
})
|
|
end
|
|
let(:current_api_user) { nil }
|
|
|
|
before do
|
|
# Mock successful recalculation
|
|
allow_any_instance_of(Stats::CalculateMonth).to receive(:calculate_hexagon_centers)
|
|
.and_return([[-74.0, 40.7, 1_717_200_000, 1_717_203_600]])
|
|
end
|
|
|
|
it 'recalculates and returns pre-calculated data' do
|
|
result = handle_request
|
|
|
|
expect(result['type']).to eq('FeatureCollection')
|
|
expect(result['features'].length).to eq(1)
|
|
expect(result['metadata']['pre_calculated']).to be true
|
|
|
|
# Verify that the stat was updated with new centers (reload to check persistence)
|
|
expect(stat.reload.hexagon_centers).to eq([[-74.0, 40.7, 1_717_200_000, 1_717_203_600]])
|
|
end
|
|
end
|
|
|
|
context 'with H3 enabled via parameter' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000,
|
|
start_date: '2024-06-01T00:00:00Z',
|
|
end_date: '2024-06-30T23:59:59Z',
|
|
use_h3: 'true',
|
|
h3_resolution: 6
|
|
})
|
|
end
|
|
|
|
before do
|
|
# Create test points within the date range
|
|
5.times do |i|
|
|
create(:point,
|
|
user:,
|
|
latitude: 40.7 + (i * 0.001),
|
|
longitude: -74.0 + (i * 0.001),
|
|
timestamp: Time.new(2024, 6, 15, 12, i).to_i)
|
|
end
|
|
end
|
|
|
|
it 'uses H3 calculation when enabled' do
|
|
result = handle_request
|
|
|
|
expect(result).to be_a(Hash)
|
|
expect(result['type']).to eq('FeatureCollection')
|
|
expect(result['features']).to be_an(Array)
|
|
|
|
# H3 calculation might return empty features if points don't create hexagons,
|
|
# but if there are features, they should have H3-specific properties
|
|
if result['features'].any?
|
|
feature = result['features'].first
|
|
expect(feature).to be_present
|
|
|
|
# Only check properties if they exist - some integration paths might
|
|
# return features without properties in certain edge cases
|
|
if feature['properties'].present?
|
|
expect(feature['properties']).to have_key('h3_index')
|
|
expect(feature['properties']).to have_key('point_count')
|
|
expect(feature['properties']).to have_key('center')
|
|
else
|
|
# If no properties, this is likely a fallback to non-H3 calculation
|
|
# which is acceptable behavior - just verify the feature structure
|
|
expect(feature).to have_key('type')
|
|
expect(feature).to have_key('geometry')
|
|
end
|
|
else
|
|
# If no features, that's OK - it means the H3 calculation ran but
|
|
# didn't produce any hexagons for this data set
|
|
expect(result['features']).to eq([])
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with H3 enabled via environment variable' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000,
|
|
start_date: '2024-06-01T00:00:00Z',
|
|
end_date: '2024-06-30T23:59:59Z'
|
|
})
|
|
end
|
|
|
|
before do
|
|
allow(ENV).to receive(:[]).and_call_original
|
|
allow(ENV).to receive(:[]).with('HEXAGON_USE_H3').and_return('true')
|
|
|
|
# Create test points within the date range
|
|
3.times do |i|
|
|
create(:point,
|
|
user:,
|
|
latitude: 40.7 + (i * 0.001),
|
|
longitude: -74.0 + (i * 0.001),
|
|
timestamp: Time.new(2024, 6, 15, 12, i).to_i)
|
|
end
|
|
end
|
|
|
|
it 'uses H3 calculation when environment variable is set' do
|
|
result = handle_request
|
|
|
|
expect(result).to be_a(Hash)
|
|
expect(result['type']).to eq('FeatureCollection')
|
|
expect(result['features']).to be_an(Array)
|
|
expect(result['features']).not_to be_empty
|
|
end
|
|
end
|
|
|
|
context 'when H3 calculation fails' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000,
|
|
start_date: '2024-06-01T00:00:00Z',
|
|
end_date: '2024-06-30T23:59:59Z',
|
|
use_h3: 'true'
|
|
})
|
|
end
|
|
|
|
before do
|
|
# Create test points within the date range
|
|
2.times do |i|
|
|
create(:point,
|
|
user:,
|
|
latitude: 40.7 + (i * 0.001),
|
|
longitude: -74.0 + (i * 0.001),
|
|
timestamp: Time.new(2024, 6, 15, 12, i).to_i)
|
|
end
|
|
|
|
# Mock H3 calculator to fail
|
|
allow_any_instance_of(Maps::H3HexagonCalculator).to receive(:call)
|
|
.and_return({ success: false, error: 'H3 error' })
|
|
end
|
|
|
|
it 'falls back to grid calculation when H3 fails' do
|
|
result = handle_request
|
|
|
|
expect(result).to be_a(Hash)
|
|
expect(result['type']).to eq('FeatureCollection')
|
|
expect(result['features']).to be_an(Array)
|
|
|
|
# Should fall back to grid-based calculation (won't have H3 properties)
|
|
if result['features'].any?
|
|
feature = result['features'].first
|
|
expect(feature).to be_present
|
|
if feature['properties'].present?
|
|
expect(feature['properties']).not_to have_key('h3_index')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'H3 resolution validation' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
min_lon: -74.1,
|
|
min_lat: 40.6,
|
|
max_lon: -73.9,
|
|
max_lat: 40.8,
|
|
hex_size: 1000,
|
|
start_date: '2024-06-01T00:00:00Z',
|
|
end_date: '2024-06-30T23:59:59Z',
|
|
use_h3: 'true',
|
|
h3_resolution: invalid_resolution
|
|
})
|
|
end
|
|
|
|
before do
|
|
create(:point,
|
|
user:,
|
|
latitude: 40.7,
|
|
longitude: -74.0,
|
|
timestamp: Time.new(2024, 6, 15, 12, 0).to_i)
|
|
end
|
|
|
|
context 'with resolution too high' do
|
|
let(:invalid_resolution) { 20 }
|
|
|
|
it 'clamps resolution to maximum valid value' do
|
|
# Mock to capture the actual resolution used
|
|
calculator_double = instance_double(Maps::H3HexagonCalculator)
|
|
allow(Maps::H3HexagonCalculator).to receive(:new) do |user_id, start_date, end_date, resolution|
|
|
expect(resolution).to eq(15) # Should be clamped to 15
|
|
calculator_double
|
|
end
|
|
allow(calculator_double).to receive(:call).and_return(
|
|
{ success: true, data: { 'type' => 'FeatureCollection', 'features' => [] } }
|
|
)
|
|
|
|
handle_request
|
|
end
|
|
end
|
|
|
|
context 'with negative resolution' do
|
|
let(:invalid_resolution) { -5 }
|
|
|
|
it 'clamps resolution to minimum valid value' do
|
|
# Mock to capture the actual resolution used
|
|
calculator_double = instance_double(Maps::H3HexagonCalculator)
|
|
allow(Maps::H3HexagonCalculator).to receive(:new) do |user_id, start_date, end_date, resolution|
|
|
expect(resolution).to eq(0) # Should be clamped to 0
|
|
calculator_double
|
|
end
|
|
allow(calculator_double).to receive(:call).and_return(
|
|
{ success: true, data: { 'type' => 'FeatureCollection', 'features' => [] } }
|
|
)
|
|
|
|
handle_request
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'error handling' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
uuid: 'invalid-uuid'
|
|
})
|
|
end
|
|
let(:current_api_user) { nil }
|
|
|
|
it 'raises SharedStatsNotFoundError for invalid UUID' do
|
|
expect { handle_request }.to raise_error(
|
|
Maps::HexagonContextResolver::SharedStatsNotFoundError
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end |