-
Start tracking your location!
-
- To start tracking your location and putting it on the map, you need to configure your mobile application.
-
-
- To do so, grab the API key from <%= link_to 'here', settings_path, class: 'link' %> and follow the instructions in the <%= link_to 'documentation', 'https://dawarich.app/docs/tutorials/track-your-location?utm_source=app&utm_medium=referral&utm_campaign=onboarding', class: 'link' %>.
-
-
+
+
+
+
+ <%= icon 'goal' %> Start Tracking Your Location!
+
+ Welcome to Dawarich! Let's get you set up to start tracking and visualizing your location data.
+
+
+
+
+
+
+
+
+
+
1
+
Download the Official App
+
+
+ Get the official Dawarich app from the App Store to start tracking your location.
+
+
+ <%= link_to 'https://apps.apple.com/de/app/dawarich/id6739544999?itscg=30200&itsct=apps_box_badge&mttnsubad=6739544999',
+ class: 'inline-block rounded-lg border-2 border-transparent hover:border-primary hover:shadow-lg hover:shadow-primary/20 transition-all duration-300 ease-in-out transform hover:scale-105' do %>
+ <%= image_tag 'Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg',
+ class: 'h-12 transition-opacity duration-300' %>
+ <% end %>
+
+
+
+
+
+
2
+
Scan QR Code to Connect
+
+
+ Scan this QR code with the Dawarich app to automatically configure your connection.
+
+
+
+ <%= api_key_qr_code(current_user, size: 3) %>
+
+
+
+
+
+
+
3
+
Manual Setup (Alternative)
+
+
+ Alternatively, you can manually grab your API key from
+ <%= link_to 'Settings', settings_path, class: 'link link-primary font-medium' %>
+ and follow the setup instructions in our
+ <%= link_to 'documentation', 'https://dawarich.app/docs/tutorials/track-your-location?utm_source=app&utm_medium=referral&utm_campaign=onboarding',
+ class: 'link link-primary font-medium', target: '_blank', rel: 'noopener' %>.
+
+
+
+
+
+
+
+
+
+ Need help? Check out our
+ <%= link_to 'documentation', 'https://dawarich.app/docs/category/tutorials?utm_source=app&utm_medium=referral&utm_campaign=onboarding',
+ class: 'link link-primary text-xs', target: '_blank', rel: 'noopener' %>
+ for more guidance.
+
+
+
+
+
<% end %>
diff --git a/config/initializers/prometheus.rb b/config/initializers/prometheus.rb
index 1a2f38e0..73650a96 100644
--- a/config/initializers/prometheus.rb
+++ b/config/initializers/prometheus.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-if !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled?
+if defined?(Rails::Server) && !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled?
require 'prometheus_exporter/middleware'
require 'prometheus_exporter/instrumentation'
diff --git a/config/schedule.yml b/config/schedule.yml
index f0fcb40a..96f3288d 100644
--- a/config/schedule.yml
+++ b/config/schedule.yml
@@ -39,3 +39,8 @@ daily_track_generation_job:
cron: "0 */4 * * *" # every 4 hours
class: "Tracks::DailyGenerationJob"
queue: tracks
+
+nightly_reverse_geocoding_job:
+ cron: "15 1 * * *" # every day at 01:15
+ class: "Points::NightlyReverseGeocodingJob"
+ queue: tracks
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 3e27ad70..8aead742 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :user do
sequence :email do |n|
- "user#{n}@example.com"
+ "user#{n}-#{Time.current.to_f}@example.com"
end
status { :active }
diff --git a/spec/jobs/area_visits_calculation_scheduling_job_spec.rb b/spec/jobs/area_visits_calculation_scheduling_job_spec.rb
index b38ee551..39fae4d7 100644
--- a/spec/jobs/area_visits_calculation_scheduling_job_spec.rb
+++ b/spec/jobs/area_visits_calculation_scheduling_job_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do
let(:area) { create(:area, user: user) }
it 'calls the AreaVisitsCalculationService' do
+ allow(User).to receive(:find_each).and_yield(user)
expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id).and_call_original
described_class.new.perform
diff --git a/spec/jobs/bulk_stats_calculating_job_spec.rb b/spec/jobs/bulk_stats_calculating_job_spec.rb
index eb59c46a..bdcc17f9 100644
--- a/spec/jobs/bulk_stats_calculating_job_spec.rb
+++ b/spec/jobs/bulk_stats_calculating_job_spec.rb
@@ -23,8 +23,6 @@ RSpec.describe BulkStatsCalculatingJob, type: :job do
end
before do
- # Remove any leftover users from other tests, keeping only our test users
- User.where.not(id: [active_user1.id, active_user2.id]).destroy_all
allow(Stats::BulkCalculator).to receive(:new).and_call_original
allow_any_instance_of(Stats::BulkCalculator).to receive(:call)
end
@@ -69,8 +67,6 @@ RSpec.describe BulkStatsCalculatingJob, type: :job do
end
before do
- # Remove any leftover users from other tests, keeping only our test users
- User.where.not(id: [trial_user1.id, trial_user2.id]).destroy_all
allow(Stats::BulkCalculator).to receive(:new).and_call_original
allow_any_instance_of(Stats::BulkCalculator).to receive(:call)
end
diff --git a/spec/jobs/bulk_visits_suggesting_job_spec.rb b/spec/jobs/bulk_visits_suggesting_job_spec.rb
index 66bf7da6..7c013dcd 100644
--- a/spec/jobs/bulk_visits_suggesting_job_spec.rb
+++ b/spec/jobs/bulk_visits_suggesting_job_spec.rb
@@ -26,6 +26,12 @@ RSpec.describe BulkVisitsSuggestingJob, type: :job do
end
it 'schedules jobs only for active users with tracked points' do
+ active_users_mock = double('ActiveRecord::Relation')
+ allow(User).to receive(:active).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:active).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:where).with(id: []).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:find_each).and_yield(user_with_points).and_yield(user)
+
expect(VisitSuggestingJob).to receive(:perform_later).with(
user_id: user_with_points.id,
start_at: time_chunks.first.first,
@@ -54,6 +60,12 @@ RSpec.describe BulkVisitsSuggestingJob, type: :job do
]
allow_any_instance_of(Visits::TimeChunks).to receive(:call).and_return(chunks)
+ active_users_mock = double('ActiveRecord::Relation')
+ allow(User).to receive(:active).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:active).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:where).with(id: []).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:find_each).and_yield(user_with_points)
+
chunks.each do |chunk|
expect(VisitSuggestingJob).to receive(:perform_later).with(
user_id: user_with_points.id,
@@ -94,6 +106,12 @@ RSpec.describe BulkVisitsSuggestingJob, type: :job do
.and_return(time_chunks_instance)
allow(time_chunks_instance).to receive(:call).and_return(custom_chunks)
+ active_users_mock = double('ActiveRecord::Relation')
+ allow(User).to receive(:active).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:active).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:where).with(id: []).and_return(active_users_mock)
+ allow(active_users_mock).to receive(:find_each).and_yield(user_with_points)
+
expect(VisitSuggestingJob).to receive(:perform_later).with(
user_id: user_with_points.id,
start_at: custom_chunks.first.first,
diff --git a/spec/jobs/points/nightly_reverse_geocoding_job_spec.rb b/spec/jobs/points/nightly_reverse_geocoding_job_spec.rb
new file mode 100644
index 00000000..37fd29d5
--- /dev/null
+++ b/spec/jobs/points/nightly_reverse_geocoding_job_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Points::NightlyReverseGeocodingJob, type: :job do
+ describe '#perform' do
+ let(:user) { create(:user) }
+
+ before do
+ # Clear any existing jobs and points to ensure test isolation
+ ActiveJob::Base.queue_adapter.enqueued_jobs.clear
+ Point.delete_all
+ end
+
+ context 'when reverse geocoding is disabled' do
+ before do
+ allow(DawarichSettings).to receive(:reverse_geocoding_enabled?).and_return(false)
+ end
+
+ let!(:point_without_geocoding) do
+ create(:point, user: user, reverse_geocoded_at: nil)
+ end
+
+ it 'does not process any points' do
+ expect_any_instance_of(Point).not_to receive(:async_reverse_geocode)
+
+ described_class.perform_now
+ end
+
+ it 'returns early without querying points' do
+ allow(Point).to receive(:not_reverse_geocoded)
+
+ described_class.perform_now
+
+ expect(Point).not_to have_received(:not_reverse_geocoded)
+ end
+
+ it 'does not enqueue any ReverseGeocodingJob jobs' do
+ expect { described_class.perform_now }.not_to have_enqueued_job(ReverseGeocodingJob)
+ end
+ end
+
+ context 'when reverse geocoding is enabled' do
+ before do
+ allow(DawarichSettings).to receive(:reverse_geocoding_enabled?).and_return(true)
+ end
+
+ context 'with no points needing reverse geocoding' do
+ let!(:geocoded_point) do
+ create(:point, user: user, reverse_geocoded_at: 1.day.ago)
+ end
+
+ it 'does not process any points' do
+ expect_any_instance_of(Point).not_to receive(:async_reverse_geocode)
+
+ described_class.perform_now
+ end
+
+ it 'does not enqueue any ReverseGeocodingJob jobs' do
+ expect { described_class.perform_now }.not_to have_enqueued_job(ReverseGeocodingJob)
+ end
+ end
+
+ context 'with points needing reverse geocoding' do
+ let!(:point_without_geocoding1) do
+ create(:point, user: user, reverse_geocoded_at: nil)
+ end
+ let!(:point_without_geocoding2) do
+ create(:point, user: user, reverse_geocoded_at: nil)
+ end
+ let!(:geocoded_point) do
+ create(:point, user: user, reverse_geocoded_at: 1.day.ago)
+ end
+
+ it 'processes all points that need reverse geocoding' do
+ expect { described_class.perform_now }.to have_enqueued_job(ReverseGeocodingJob).exactly(2).times
+ end
+
+ it 'enqueues jobs with correct parameters' do
+ expect { described_class.perform_now }
+ .to have_enqueued_job(ReverseGeocodingJob)
+ .with('Point', point_without_geocoding1.id)
+ .and have_enqueued_job(ReverseGeocodingJob)
+ .with('Point', point_without_geocoding2.id)
+ end
+
+ it 'uses find_each with correct batch size' do
+ relation_mock = double('ActiveRecord::Relation')
+ allow(Point).to receive(:not_reverse_geocoded).and_return(relation_mock)
+ allow(relation_mock).to receive(:find_each).with(batch_size: 1000)
+
+ described_class.perform_now
+
+ expect(relation_mock).to have_received(:find_each).with(batch_size: 1000)
+ end
+ end
+
+ context 'with large number of points needing reverse geocoding' do
+ before do
+ # Create 2500 points to test batching
+ points_data = (1..2500).map do |i|
+ {
+ user_id: user.id,
+ latitude: 40.7128 + (i * 0.0001),
+ longitude: -74.0060 + (i * 0.0001),
+ timestamp: Time.current.to_i + i,
+ lonlat: "POINT(#{-74.0060 + (i * 0.0001)} #{40.7128 + (i * 0.0001)})",
+ reverse_geocoded_at: nil,
+ created_at: Time.current,
+ updated_at: Time.current
+ }
+ end
+ Point.insert_all(points_data)
+ end
+
+ it 'processes all points in batches' do
+ expect { described_class.perform_now }.to have_enqueued_job(ReverseGeocodingJob).exactly(2500).times
+ end
+
+ it 'uses efficient batching to avoid memory issues' do
+ relation_mock = double('ActiveRecord::Relation')
+ allow(Point).to receive(:not_reverse_geocoded).and_return(relation_mock)
+ allow(relation_mock).to receive(:find_each).with(batch_size: 1000)
+
+ described_class.perform_now
+
+ expect(relation_mock).to have_received(:find_each).with(batch_size: 1000)
+ end
+ end
+ end
+
+ describe 'queue configuration' do
+ it 'uses the reverse_geocoding queue' do
+ expect(described_class.queue_name).to eq('reverse_geocoding')
+ end
+ end
+
+ describe 'error handling' do
+ before do
+ allow(DawarichSettings).to receive(:reverse_geocoding_enabled?).and_return(true)
+ end
+
+ let!(:point_without_geocoding) do
+ create(:point, user: user, reverse_geocoded_at: nil)
+ end
+
+ context 'when a point fails to reverse geocode' do
+ before do
+ allow_any_instance_of(Point).to receive(:async_reverse_geocode).and_raise(StandardError, 'API error')
+ end
+
+ it 'continues processing other points despite individual failures' do
+ expect { described_class.perform_now }.to raise_error(StandardError, 'API error')
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/jobs/tracks/daily_generation_job_spec.rb b/spec/jobs/tracks/daily_generation_job_spec.rb
index c23d9243..284bfd1d 100644
--- a/spec/jobs/tracks/daily_generation_job_spec.rb
+++ b/spec/jobs/tracks/daily_generation_job_spec.rb
@@ -26,6 +26,11 @@ RSpec.describe Tracks::DailyGenerationJob, type: :job do
active_user.update!(points_count: active_user.points.count)
trial_user.update!(points_count: trial_user.points.count)
+ # Mock User.active_or_trial to only return test users
+ active_or_trial_mock = double('ActiveRecord::Relation')
+ allow(User).to receive(:active_or_trial).and_return(active_or_trial_mock)
+ allow(active_or_trial_mock).to receive(:find_each).and_yield(active_user).and_yield(trial_user)
+
ActiveJob::Base.queue_adapter.enqueued_jobs.clear
end
diff --git a/spec/mailers/users_mailer_spec.rb b/spec/mailers/users_mailer_spec.rb
index 9d0195e3..558c3c48 100644
--- a/spec/mailers/users_mailer_spec.rb
+++ b/spec/mailers/users_mailer_spec.rb
@@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe UsersMailer, type: :mailer do
- let(:user) { create(:user, email: 'test@example.com') }
+ let(:user) { create(:user) }
before do
stub_const('ENV', ENV.to_hash.merge('SMTP_FROM' => 'hi@dawarich.app'))
@@ -14,11 +14,11 @@ RSpec.describe UsersMailer, type: :mailer do
it 'renders the headers' do
expect(mail.subject).to eq('Welcome to Dawarich!')
- expect(mail.to).to eq(['test@example.com'])
+ expect(mail.to).to eq([user.email])
end
it 'renders the body' do
- expect(mail.body.encoded).to match('test@example.com')
+ expect(mail.body.encoded).to match(user.email)
end
end
@@ -27,7 +27,7 @@ RSpec.describe UsersMailer, type: :mailer do
it 'renders the headers' do
expect(mail.subject).to eq('Explore Dawarich features!')
- expect(mail.to).to eq(['test@example.com'])
+ expect(mail.to).to eq([user.email])
end
end
@@ -36,7 +36,7 @@ RSpec.describe UsersMailer, type: :mailer do
it 'renders the headers' do
expect(mail.subject).to eq('⚠️ Your Dawarich trial expires in 2 days')
- expect(mail.to).to eq(['test@example.com'])
+ expect(mail.to).to eq([user.email])
end
end
@@ -45,7 +45,7 @@ RSpec.describe UsersMailer, type: :mailer do
it 'renders the headers' do
expect(mail.subject).to eq('💔 Your Dawarich trial expired')
- expect(mail.to).to eq(['test@example.com'])
+ expect(mail.to).to eq([user.email])
end
end
@@ -54,7 +54,7 @@ RSpec.describe UsersMailer, type: :mailer do
it 'renders the headers' do
expect(mail.subject).to eq('🚀 Still interested in Dawarich? Subscribe now!')
- expect(mail.to).to eq(['test@example.com'])
+ expect(mail.to).to eq([user.email])
end
end
@@ -63,7 +63,7 @@ RSpec.describe UsersMailer, type: :mailer do
it 'renders the headers' do
expect(mail.subject).to eq('📍 Your location data is waiting - Subscribe to Dawarich')
- expect(mail.to).to eq(['test@example.com'])
+ expect(mail.to).to eq([user.email])
end
end
end
diff --git a/spec/serializers/api/user_serializer_spec.rb b/spec/serializers/api/user_serializer_spec.rb
index 178c64e0..d4612fe9 100644
--- a/spec/serializers/api/user_serializer_spec.rb
+++ b/spec/serializers/api/user_serializer_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Api::UserSerializer do
describe '#call' do
subject(:serializer) { described_class.new(user).call }
- let(:user) { create(:user, email: 'test@example.com', theme: 'dark') }
+ let(:user) { create(:user) }
it 'returns JSON with correct user attributes' do
expect(serializer[:user][:email]).to eq(user.email)
diff --git a/spec/services/areas/visits/create_spec.rb b/spec/services/areas/visits/create_spec.rb
index 18865d6a..f66064ab 100644
--- a/spec/services/areas/visits/create_spec.rb
+++ b/spec/services/areas/visits/create_spec.rb
@@ -4,7 +4,7 @@ require 'rails_helper'
RSpec.describe Areas::Visits::Create do
describe '#call' do
- let(:user) { create(:user) }
+ let!(:user) { create(:user) }
let(:home_area) { create(:area, user:, latitude: 0, longitude: 0, radius: 100) }
let(:work_area) { create(:area, user:, latitude: 1, longitude: 1, radius: 100) }
diff --git a/spec/services/google_maps/phone_takeout_importer_spec.rb b/spec/services/google_maps/phone_takeout_importer_spec.rb
index 301590d4..d35ea598 100644
--- a/spec/services/google_maps/phone_takeout_importer_spec.rb
+++ b/spec/services/google_maps/phone_takeout_importer_spec.rb
@@ -39,13 +39,13 @@ RSpec.describe GoogleMaps::PhoneTakeoutImporter do
it 'creates points with correct data' do
parser
- expect(Point.all[6].lat).to eq(27.696576)
- expect(Point.all[6].lon).to eq(-97.376949)
- expect(Point.all[6].timestamp).to eq(1_693_180_140)
+ expect(user.points[6].lat).to eq(27.696576)
+ expect(user.points[6].lon).to eq(-97.376949)
+ expect(user.points[6].timestamp).to eq(1_693_180_140)
- expect(Point.last.lat).to eq(27.709617)
- expect(Point.last.lon).to eq(-97.375988)
- expect(Point.last.timestamp).to eq(1_693_180_320)
+ expect(user.points.last.lat).to eq(27.709617)
+ expect(user.points.last.lon).to eq(-97.375988)
+ expect(user.points.last.timestamp).to eq(1_693_180_320)
end
end
end
diff --git a/spec/services/gpx/track_importer_spec.rb b/spec/services/gpx/track_importer_spec.rb
index 5aeb7117..341e0fc3 100644
--- a/spec/services/gpx/track_importer_spec.rb
+++ b/spec/services/gpx/track_importer_spec.rb
@@ -57,11 +57,13 @@ RSpec.describe Gpx::TrackImporter do
it 'creates points with correct data' do
parser
- expect(Point.first.lat).to eq(37.1722103)
- expect(Point.first.lon).to eq(-3.55468)
- expect(Point.first.altitude).to eq(1066)
- expect(Point.first.timestamp).to eq(Time.zone.parse('2024-04-21T10:19:55Z').to_i)
- expect(Point.first.velocity).to eq('2.9')
+ point = user.points.first
+
+ expect(point.lat).to eq(37.1722103)
+ expect(point.lon).to eq(-3.55468)
+ expect(point.altitude).to eq(1066)
+ expect(point.timestamp).to eq(Time.zone.parse('2024-04-21T10:19:55Z').to_i)
+ expect(point.velocity).to eq('2.9')
end
end
@@ -71,11 +73,13 @@ RSpec.describe Gpx::TrackImporter do
it 'creates points with correct data' do
parser
- expect(Point.first.lat).to eq(10.758321212464024)
- expect(Point.first.lon).to eq(106.64234449272531)
- expect(Point.first.altitude).to eq(17)
- expect(Point.first.timestamp).to eq(1_730_626_211)
- expect(Point.first.velocity).to eq('2.8')
+ point = user.points.first
+
+ expect(point.lat).to eq(10.758321212464024)
+ expect(point.lon).to eq(106.64234449272531)
+ expect(point.altitude).to eq(17)
+ expect(point.timestamp).to eq(1_730_626_211)
+ expect(point.velocity).to eq('2.8')
end
end
diff --git a/spec/services/maps/bounds_calculator_spec.rb b/spec/services/maps/bounds_calculator_spec.rb
index a48ec8bb..d4e28cf5 100644
--- a/spec/services/maps/bounds_calculator_spec.rb
+++ b/spec/services/maps/bounds_calculator_spec.rb
@@ -5,11 +5,11 @@ require 'rails_helper'
RSpec.describe Maps::BoundsCalculator do
describe '.call' do
subject(:calculate_bounds) do
- described_class.call(
+ described_class.new(
target_user: target_user,
start_date: start_date,
end_date: end_date
- )
+ ).call
end
let(:user) { create(:user) }
@@ -29,16 +29,18 @@ RSpec.describe Maps::BoundsCalculator do
end
it 'returns success with bounds data' do
- expect(calculate_bounds).to match({
- success: true,
- data: {
- min_lat: 40.6,
- max_lat: 40.8,
- min_lng: -74.1,
- max_lng: -73.9,
- point_count: 3
+ expect(calculate_bounds).to match(
+ {
+ success: true,
+ data: {
+ min_lat: 40.6,
+ max_lat: 40.8,
+ min_lng: -74.1,
+ max_lng: -73.9,
+ point_count: 3
+ }
}
- })
+ )
end
end
@@ -50,11 +52,13 @@ RSpec.describe Maps::BoundsCalculator do
end
it 'returns failure with no data message' do
- expect(calculate_bounds).to match({
- success: false,
- error: 'No data found for the specified date range',
- point_count: 0
- })
+ expect(calculate_bounds).to match(
+ {
+ success: false,
+ error: 'No data found for the specified date range',
+ point_count: 0
+ }
+ )
end
end
@@ -117,4 +121,4 @@ RSpec.describe Maps::BoundsCalculator do
end
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/services/maps/date_parameter_coercer_spec.rb b/spec/services/maps/date_parameter_coercer_spec.rb
index 107147ae..ac91210d 100644
--- a/spec/services/maps/date_parameter_coercer_spec.rb
+++ b/spec/services/maps/date_parameter_coercer_spec.rb
@@ -4,7 +4,7 @@ require 'rails_helper'
RSpec.describe Maps::DateParameterCoercer do
describe '.call' do
- subject(:coerce_date) { described_class.call(param) }
+ subject(:coerce_date) { described_class.new(param).call }
context 'with integer parameter' do
let(:param) { 1_717_200_000 }
@@ -67,4 +67,4 @@ RSpec.describe Maps::DateParameterCoercer do
end
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/services/maps/hexagon_request_handler_spec.rb b/spec/services/maps/hexagon_request_handler_spec.rb
index 1dd6223c..7cef2727 100644
--- a/spec/services/maps/hexagon_request_handler_spec.rb
+++ b/spec/services/maps/hexagon_request_handler_spec.rb
@@ -17,39 +17,36 @@ RSpec.describe Maps::HexagonRequestHandler do
before do
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
+
+ # Clean up database state to avoid conflicts - order matters due to foreign keys
+ Point.delete_all
+ Stat.delete_all
+ User.delete_all
end
- context 'with authenticated user and bounding box params' do
+ context 'with authenticated user but no pre-calculated data' 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'
- })
+ 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
+ it 'returns empty feature collection when no pre-calculated data' 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
+ expect(result['features']).to eq([])
+ expect(result['metadata']['hexagon_count']).to eq(0)
+ expect(result['metadata']['source']).to eq('pre_calculated')
end
end
@@ -65,14 +62,16 @@ RSpec.describe Maps::HexagonRequestHandler do
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
- })
+ 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 }
@@ -89,35 +88,26 @@ RSpec.describe Maps::HexagonRequestHandler do
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
- })
+ 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
+ it 'returns empty feature collection when no pre-calculated centers' 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
+ expect(result['features']).to eq([])
+ expect(result['metadata']['hexagon_count']).to eq(0)
+ expect(result['metadata']['source']).to eq('pre_calculated')
end
end
@@ -127,14 +117,16 @@ RSpec.describe Maps::HexagonRequestHandler do
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
- })
+ 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 }
@@ -156,214 +148,14 @@ RSpec.describe Maps::HexagonRequestHandler do
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'
- })
+ ActionController::Parameters.new(
+ {
+ uuid: 'invalid-uuid'
+ }
+ )
end
let(:current_api_user) { nil }
@@ -374,4 +166,4 @@ RSpec.describe Maps::HexagonRequestHandler do
end
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/services/own_tracks/importer_spec.rb b/spec/services/own_tracks/importer_spec.rb
index cc9a9713..3305c9eb 100644
--- a/spec/services/own_tracks/importer_spec.rb
+++ b/spec/services/own_tracks/importer_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe OwnTracks::Importer do
it 'correctly writes attributes' do
parser
- point = Point.first
+ point = user.points.first
expect(point.lonlat.x).to be_within(0.001).of(13.332)
expect(point.lonlat.y).to be_within(0.001).of(52.225)
expect(point.attributes.except('lonlat')).to include(
@@ -75,7 +75,7 @@ RSpec.describe OwnTracks::Importer do
it 'correctly converts speed' do
parser
- expect(Point.first.velocity).to eq('1.4')
+ expect(user.points.first.velocity).to eq('1.4')
end
end
diff --git a/spec/services/photos/importer_spec.rb b/spec/services/photos/importer_spec.rb
index 567898a3..67dd9b58 100644
--- a/spec/services/photos/importer_spec.rb
+++ b/spec/services/photos/importer_spec.rb
@@ -30,15 +30,18 @@ RSpec.describe Photos::Importer do
it 'creates points with correct attributes' do
service
- expect(Point.first.lat.to_f).to eq(59.0000)
- expect(Point.first.lon.to_f).to eq(30.0000)
- expect(Point.first.timestamp).to eq(978_296_400)
- expect(Point.first.import_id).to eq(import.id)
+ first_point = user.points.first
+ second_point = user.points.second
- expect(Point.second.lat.to_f).to eq(55.0001)
- expect(Point.second.lon.to_f).to eq(37.0001)
- expect(Point.second.timestamp).to eq(978_296_400)
- expect(Point.second.import_id).to eq(import.id)
+ expect(first_point.lat.to_f).to eq(59.0000)
+ expect(first_point.lon.to_f).to eq(30.0000)
+ expect(first_point.timestamp).to eq(978_296_400)
+ expect(first_point.import_id).to eq(import.id)
+
+ expect(second_point.lat.to_f).to eq(55.0001)
+ expect(second_point.lon.to_f).to eq(37.0001)
+ expect(second_point.timestamp).to eq(978_296_400)
+ expect(second_point.import_id).to eq(import.id)
end
end
diff --git a/spec/system/map_interaction_spec.rb b/spec/system/map_interaction_spec.rb
index 4d616a4e..43dc9e41 100644
--- a/spec/system/map_interaction_spec.rb
+++ b/spec/system/map_interaction_spec.rb
@@ -15,22 +15,20 @@ RSpec.describe 'Map Interaction', type: :system do
# Create a series of points that form a route
[
create(:point, user: user,
- lonlat: "POINT(13.404954 52.520008)",
+ lonlat: 'POINT(13.404954 52.520008)',
timestamp: 1.hour.ago.to_i, velocity: 10, battery: 80),
create(:point, user: user,
- lonlat: "POINT(13.405954 52.521008)",
+ lonlat: 'POINT(13.405954 52.521008)',
timestamp: 50.minutes.ago.to_i, velocity: 15, battery: 78),
create(:point, user: user,
- lonlat: "POINT(13.406954 52.522008)",
+ lonlat: 'POINT(13.406954 52.522008)',
timestamp: 40.minutes.ago.to_i, velocity: 12, battery: 76),
create(:point, user: user,
- lonlat: "POINT(13.407954 52.523008)",
+ lonlat: 'POINT(13.407954 52.523008)',
timestamp: 30.minutes.ago.to_i, velocity: 8, battery: 74)
]
end
-
-
describe 'Map page interaction' do
context 'when user is signed in' do
include_context 'authenticated map user'
@@ -127,7 +125,7 @@ RSpec.describe 'Map Interaction', type: :system do
# The calendar panel JavaScript interaction is complex and may not work
# reliably in headless test environment, but the button should be functional
- puts "Note: Calendar button is functional. Panel interaction may require manual testing."
+ puts 'Note: Calendar button is functional. Panel interaction may require manual testing.'
end
end
@@ -207,28 +205,30 @@ RSpec.describe 'Map Interaction', type: :system do
else
# If we can't trigger the popup, at least verify the setup is correct
expect(user_settings.dig('maps', 'distance_unit')).to eq('km')
- puts "Note: Polyline popup interaction could not be triggered in test environment"
+ puts 'Note: Polyline popup interaction could not be triggered in test environment'
end
end
end
- context 'with miles distance unit' do
- let(:user_with_miles) { create(:user, settings: { 'maps' => { 'distance_unit' => 'mi' } }, password: 'password123') }
+ context 'with miles distance unit' do
+ let(:user_with_miles) do
+ create(:user, settings: { 'maps' => { 'distance_unit' => 'mi' } }, password: 'password123')
+ end
let!(:points_for_miles_user) do
# Create a series of points that form a route for the miles user
[
create(:point, user: user_with_miles,
- lonlat: "POINT(13.404954 52.520008)",
+ lonlat: 'POINT(13.404954 52.520008)',
timestamp: 1.hour.ago.to_i, velocity: 10, battery: 80),
create(:point, user: user_with_miles,
- lonlat: "POINT(13.405954 52.521008)",
+ lonlat: 'POINT(13.405954 52.521008)',
timestamp: 50.minutes.ago.to_i, velocity: 15, battery: 78),
create(:point, user: user_with_miles,
- lonlat: "POINT(13.406954 52.522008)",
+ lonlat: 'POINT(13.406954 52.522008)',
timestamp: 40.minutes.ago.to_i, velocity: 12, battery: 76),
create(:point, user: user_with_miles,
- lonlat: "POINT(13.407954 52.523008)",
+ lonlat: 'POINT(13.407954 52.523008)',
timestamp: 30.minutes.ago.to_i, velocity: 8, battery: 74)
]
end
@@ -280,7 +280,7 @@ RSpec.describe 'Map Interaction', type: :system do
else
# If we can't trigger the popup, at least verify the setup is correct
expect(user_settings.dig('maps', 'distance_unit')).to eq('mi')
- puts "Note: Polyline popup interaction could not be triggered in test environment"
+ puts 'Note: Polyline popup interaction could not be triggered in test environment'
end
end
end
@@ -288,22 +288,24 @@ RSpec.describe 'Map Interaction', type: :system do
context 'polyline popup content' do
context 'with km distance unit' do
- let(:user_with_km) { create(:user, settings: { 'maps' => { 'distance_unit' => 'km' } }, password: 'password123') }
+ let(:user_with_km) do
+ create(:user, settings: { 'maps' => { 'distance_unit' => 'km' } }, password: 'password123')
+ end
let!(:points_for_km_user) do
# Create a series of points that form a route for the km user
[
create(:point, user: user_with_km,
- lonlat: "POINT(13.404954 52.520008)",
+ lonlat: 'POINT(13.404954 52.520008)',
timestamp: 1.hour.ago.to_i, velocity: 10, battery: 80),
create(:point, user: user_with_km,
- lonlat: "POINT(13.405954 52.521008)",
+ lonlat: 'POINT(13.405954 52.521008)',
timestamp: 50.minutes.ago.to_i, velocity: 15, battery: 78),
create(:point, user: user_with_km,
- lonlat: "POINT(13.406954 52.522008)",
+ lonlat: 'POINT(13.406954 52.522008)',
timestamp: 40.minutes.ago.to_i, velocity: 12, battery: 76),
create(:point, user: user_with_km,
- lonlat: "POINT(13.407954 52.523008)",
+ lonlat: 'POINT(13.407954 52.523008)',
timestamp: 30.minutes.ago.to_i, velocity: 8, battery: 74)
]
end
@@ -356,28 +358,30 @@ RSpec.describe 'Map Interaction', type: :system do
else
# If we can't trigger the popup, at least verify the setup is correct
expect(user_settings.dig('maps', 'distance_unit')).to eq('km')
- puts "Note: Polyline popup interaction could not be triggered in test environment"
+ puts 'Note: Polyline popup interaction could not be triggered in test environment'
end
end
end
context 'with miles distance unit' do
- let(:user_with_miles) { create(:user, settings: { 'maps' => { 'distance_unit' => 'mi' } }, password: 'password123') }
+ let(:user_with_miles) do
+ create(:user, settings: { 'maps' => { 'distance_unit' => 'mi' } }, password: 'password123')
+ end
let!(:points_for_miles_user) do
# Create a series of points that form a route for the miles user
[
create(:point, user: user_with_miles,
- lonlat: "POINT(13.404954 52.520008)",
+ lonlat: 'POINT(13.404954 52.520008)',
timestamp: 1.hour.ago.to_i, velocity: 10, battery: 80),
create(:point, user: user_with_miles,
- lonlat: "POINT(13.405954 52.521008)",
+ lonlat: 'POINT(13.405954 52.521008)',
timestamp: 50.minutes.ago.to_i, velocity: 15, battery: 78),
create(:point, user: user_with_miles,
- lonlat: "POINT(13.406954 52.522008)",
+ lonlat: 'POINT(13.406954 52.522008)',
timestamp: 40.minutes.ago.to_i, velocity: 12, battery: 76),
create(:point, user: user_with_miles,
- lonlat: "POINT(13.407954 52.523008)",
+ lonlat: 'POINT(13.407954 52.523008)',
timestamp: 30.minutes.ago.to_i, velocity: 8, battery: 74)
]
end
@@ -429,7 +433,7 @@ RSpec.describe 'Map Interaction', type: :system do
else
# If we can't trigger the popup, at least verify the setup is correct
expect(user_settings.dig('maps', 'distance_unit')).to eq('mi')
- puts "Note: Polyline popup interaction could not be triggered in test environment"
+ puts 'Note: Polyline popup interaction could not be triggered in test environment'
end
end
end
@@ -456,7 +460,7 @@ RSpec.describe 'Map Interaction', type: :system do
click_button 'Update'
end
- # Wait for success flash message
+ # Wait for success flash message
expect(page).to have_css('#flash-messages', text: 'Settings updated', wait: 10)
end
@@ -710,13 +714,13 @@ RSpec.describe 'Map Interaction', type: :system do
it 'allows year selection and month navigation' do
# This test is skipped due to calendar panel JavaScript interaction issues
# The calendar button exists but the panel doesn't open reliably in test environment
- skip "Calendar panel JavaScript interaction needs debugging"
+ skip 'Calendar panel JavaScript interaction needs debugging'
end
it 'displays visited cities information' do
# This test is skipped due to calendar panel JavaScript interaction issues
# The calendar button exists but the panel doesn't open reliably in test environment
- skip "Calendar panel JavaScript interaction needs debugging"
+ skip 'Calendar panel JavaScript interaction needs debugging'
end
xit 'persists panel state in localStorage' do