diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index cf9a67cd..f335d6ef 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -31,7 +31,8 @@ class MapController < ApplicationController def build_tracks track_ids = extract_track_ids - TrackSerializer.new(current_user, track_ids).call + + TracksSerializer.new(current_user, track_ids).call end def calculate_distance diff --git a/app/jobs/tracks/create_job.rb b/app/jobs/tracks/create_job.rb index a57a3f79..919e5f82 100644 --- a/app/jobs/tracks/create_job.rb +++ b/app/jobs/tracks/create_job.rb @@ -6,20 +6,7 @@ class Tracks::CreateJob < ApplicationJob def perform(user_id, start_at: nil, end_at: nil, mode: :daily) user = User.find(user_id) - # Translate mode parameter to Generator mode - generator_mode = case mode - when :daily then :daily - when :none then :incremental - else :bulk - end - - # Generate tracks and get the count of tracks created - tracks_created = Tracks::Generator.new( - user, - start_at: start_at, - end_at: end_at, - mode: generator_mode - ).call + tracks_created = Tracks::Generator.new(user, start_at:, end_at:, mode:).call create_success_notification(user, tracks_created) rescue StandardError => e diff --git a/app/models/concerns/distance_convertible.rb b/app/models/concerns/distance_convertible.rb index 52054b3d..2a757303 100644 --- a/app/models/concerns/distance_convertible.rb +++ b/app/models/concerns/distance_convertible.rb @@ -19,7 +19,6 @@ # track.distance # => 5000 (meters stored in DB) # track.distance_in_unit('km') # => 5.0 (converted to km) # track.distance_in_unit('mi') # => 3.11 (converted to miles) -# track.formatted_distance('km') # => "5.0 km" # module DistanceConvertible extend ActiveSupport::Concern @@ -38,21 +37,11 @@ module DistanceConvertible distance.to_f / conversion_factor end - def formatted_distance(unit, precision: 2) - converted_distance = distance_in_unit(unit) - "#{converted_distance.round(precision)} #{unit}" - end - def distance_for_user(user) user_unit = user.safe_settings.distance_unit distance_in_unit(user_unit) end - def formatted_distance_for_user(user, precision: 2) - user_unit = user.safe_settings.distance_unit - formatted_distance(user_unit, precision: precision) - end - module ClassMethods def convert_distance(distance_meters, unit) return 0.0 unless distance_meters.present? @@ -66,10 +55,5 @@ module DistanceConvertible distance_meters.to_f / conversion_factor end - - def format_distance(distance_meters, unit, precision: 2) - converted = convert_distance(distance_meters, unit) - "#{converted.round(precision)} #{unit}" - end end end diff --git a/app/models/point.rb b/app/models/point.rb index f45607d7..75566be3 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -34,7 +34,7 @@ class Point < ApplicationRecord after_create :set_country after_create_commit :broadcast_coordinates after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? } - after_commit :recalculate_track, on: :update + after_commit :recalculate_track, on: :update, if: -> { track.present? } def self.without_raw_data select(column_names - ['raw_data']) @@ -99,8 +99,6 @@ class Point < ApplicationRecord end def recalculate_track - return unless track.present? - track.recalculate_path_and_distance! end diff --git a/app/serializers/track_serializer.rb b/app/serializers/track_serializer.rb index 1a67ccba..9674db0b 100644 --- a/app/serializers/track_serializer.rb +++ b/app/serializers/track_serializer.rb @@ -1,38 +1,23 @@ # frozen_string_literal: true class TrackSerializer - def initialize(user, track_ids) - @user = user - @track_ids = track_ids + def initialize(track) + @track = track end def call - return [] if track_ids.empty? - - tracks = user.tracks - .where(id: track_ids) - .order(start_at: :asc) - - tracks.map { |track| serialize_track_data(track) } - end - - private - - attr_reader :user, :track_ids - - def serialize_track_data(track) { - id: track.id, - start_at: track.start_at.iso8601, - end_at: track.end_at.iso8601, - distance: track.distance.to_i, - avg_speed: track.avg_speed.to_f, - duration: track.duration, - elevation_gain: track.elevation_gain, - elevation_loss: track.elevation_loss, - elevation_max: track.elevation_max, - elevation_min: track.elevation_min, - original_path: track.original_path.to_s + id: @track.id, + start_at: @track.start_at.iso8601, + end_at: @track.end_at.iso8601, + distance: @track.distance.to_i, + avg_speed: @track.avg_speed.to_f, + duration: @track.duration, + elevation_gain: @track.elevation_gain, + elevation_loss: @track.elevation_loss, + elevation_max: @track.elevation_max, + elevation_min: @track.elevation_min, + original_path: @track.original_path.to_s } end end diff --git a/app/serializers/tracks_serializer.rb b/app/serializers/tracks_serializer.rb new file mode 100644 index 00000000..79aeaddf --- /dev/null +++ b/app/serializers/tracks_serializer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class TracksSerializer + def initialize(user, track_ids) + @user = user + @track_ids = track_ids + end + + def call + return [] if track_ids.empty? + + tracks = user.tracks + .where(id: track_ids) + .order(start_at: :asc) + + tracks.map { |track| TrackSerializer.new(track).call } + end + + private + + attr_reader :user, :track_ids +end diff --git a/app/services/places/name_fetcher.rb b/app/services/places/name_fetcher.rb index 3a817dda..d4e01b9e 100644 --- a/app/services/places/name_fetcher.rb +++ b/app/services/places/name_fetcher.rb @@ -7,7 +7,7 @@ module Places end def call - geodata = Geocoder.search([@place.lat, @place.lon], units: :km, limit: 1, distance_sort: true).first + geodata = Geocoder.search([place.lat, place.lon], units: :km, limit: 1, distance_sort: true).first return if geodata.blank? @@ -15,21 +15,29 @@ module Places return if properties.blank? ActiveRecord::Base.transaction do - @place.name = properties['name'] if properties['name'].present? - @place.city = properties['city'] if properties['city'].present? - @place.country = properties['country'] if properties['country'].present? - @place.geodata = geodata.data if DawarichSettings.store_geodata? - @place.save! + update_place_name(properties, geodata) - if properties['name'].present? - @place - .visits - .where(name: Place::DEFAULT_NAME) - .update_all(name: properties['name']) - end + update_visits_name(properties) if properties['name'].present? - @place + place end end + + private + + attr_reader :place + + def update_place_name(properties, geodata) + place.name = properties['name'] if properties['name'].present? + place.city = properties['city'] if properties['city'].present? + place.country = properties['country'] if properties['country'].present? + place.geodata = geodata.data if DawarichSettings.store_geodata? + + place.save! + end + + def update_visits_name(properties) + place.visits.where(name: Place::DEFAULT_NAME).update_all(name: properties['name']) + end end end diff --git a/app/services/tracks/generator.rb b/app/services/tracks/generator.rb index be16b48f..9ffcdbb7 100644 --- a/app/services/tracks/generator.rb +++ b/app/services/tracks/generator.rb @@ -48,6 +48,7 @@ class Tracks::Generator Rails.logger.debug "Generator: created #{segments.size} segments" tracks_created = 0 + segments.each do |segment| track = create_track_from_segment(segment) tracks_created += 1 if track @@ -146,10 +147,6 @@ class Tracks::Generator day.beginning_of_day.to_i..day.end_of_day.to_i end - def incremental_mode? - mode == :incremental - end - def clean_existing_tracks case mode when :bulk then clean_bulk_tracks diff --git a/app/services/tracks/incremental_processor.rb b/app/services/tracks/incremental_processor.rb index 3f3bcd8b..62c1faed 100644 --- a/app/services/tracks/incremental_processor.rb +++ b/app/services/tracks/incremental_processor.rb @@ -36,12 +36,7 @@ class Tracks::IncrementalProcessor start_at = find_start_time end_at = find_end_time - Tracks::CreateJob.perform_later( - user.id, - start_at: start_at, - end_at: end_at, - mode: :none - ) + Tracks::CreateJob.perform_later(user.id, start_at:, end_at:, mode: :incremental) end private diff --git a/app/services/tracks/segmentation.rb b/app/services/tracks/segmentation.rb index 8b93dee4..57ca3b03 100644 --- a/app/services/tracks/segmentation.rb +++ b/app/services/tracks/segmentation.rb @@ -77,7 +77,7 @@ module Tracks::Segmentation return true if time_diff_seconds > time_threshold_seconds # Check distance threshold - convert km to meters to match frontend logic - distance_km = calculate_distance_kilometers_between_points(previous_point, current_point) + distance_km = calculate_km_distance_between_points(previous_point, current_point) distance_meters = distance_km * 1000 # Convert km to meters return true if distance_meters > distance_threshold_meters @@ -85,7 +85,7 @@ module Tracks::Segmentation false end - def calculate_distance_kilometers_between_points(point1, point2) + def calculate_km_distance_between_points(point1, point2) lat1, lon1 = point_coordinates(point1) lat2, lon2 = point_coordinates(point2) diff --git a/app/services/users/safe_settings.rb b/app/services/users/safe_settings.rb index 308121e5..43b6fcac 100644 --- a/app/services/users/safe_settings.rb +++ b/app/services/users/safe_settings.rb @@ -113,7 +113,6 @@ class Users::SafeSettings end def distance_unit - # km or mi settings.dig('maps', 'distance_unit') end diff --git a/app/views/settings/background_jobs/index.html.erb b/app/views/settings/background_jobs/index.html.erb index b29c6e23..22813e2a 100644 --- a/app/views/settings/background_jobs/index.html.erb +++ b/app/views/settings/background_jobs/index.html.erb @@ -50,7 +50,7 @@ -
+

Visits suggestions

Enable or disable visits suggestions. It's a background task that runs every day at midnight. Disabling it might be useful if you don't want to receive visits suggestions or if you're using the Dawarich iOS app, which has its own visits suggestions.

diff --git a/config/initializers/01_constants.rb b/config/initializers/01_constants.rb index 494f1116..594cadb3 100644 --- a/config/initializers/01_constants.rb +++ b/config/initializers/01_constants.rb @@ -5,11 +5,11 @@ SELF_HOSTED = ENV.fetch('SELF_HOSTED', 'true') == 'true' MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i DISTANCE_UNITS = { - km: 1000, # to meters + km: 1000, # to meters mi: 1609.34, # to meters - m: 1, # already in meters - ft: 0.3048, # to meters - yd: 0.9144 # to meters + m: 1, # already in meters + ft: 0.3048, # to meters + yd: 0.9144 # to meters }.freeze APP_VERSION = File.read('.app_version').strip diff --git a/spec/jobs/area_visits_calculation_scheduling_job_spec.rb b/spec/jobs/area_visits_calculation_scheduling_job_spec.rb index c2e1bbeb..b38ee551 100644 --- a/spec/jobs/area_visits_calculation_scheduling_job_spec.rb +++ b/spec/jobs/area_visits_calculation_scheduling_job_spec.rb @@ -4,13 +4,13 @@ require 'rails_helper' RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do describe '#perform' do - let!(:user) { create(:user) } - let!(:area) { create(:area, user: user) } + let(:user) { create(:user) } + let(:area) { create(:area, user: user) } it 'calls the AreaVisitsCalculationService' do - expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id) + expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id).and_call_original - described_class.new.perform_now + described_class.new.perform end end end diff --git a/spec/jobs/tracks/create_job_spec.rb b/spec/jobs/tracks/create_job_spec.rb index 69a47fa2..7303ed3d 100644 --- a/spec/jobs/tracks/create_job_spec.rb +++ b/spec/jobs/tracks/create_job_spec.rb @@ -14,12 +14,10 @@ RSpec.describe Tracks::CreateJob, type: :job do allow(generator_instance).to receive(:call) allow(Notifications::Create).to receive(:new).and_return(notification_service) allow(notification_service).to receive(:call) + allow(generator_instance).to receive(:call).and_return(2) end it 'calls the generator and creates a notification' do - # Mock the generator to return the count of tracks created - allow(generator_instance).to receive(:call).and_return(2) - described_class.new.perform(user.id) expect(Tracks::Generator).to have_received(:new).with( @@ -48,12 +46,10 @@ RSpec.describe Tracks::CreateJob, type: :job do allow(generator_instance).to receive(:call) allow(Notifications::Create).to receive(:new).and_return(notification_service) allow(notification_service).to receive(:call) + allow(generator_instance).to receive(:call).and_return(1) end it 'passes custom parameters to the generator' do - # Mock generator to return the count of tracks created - allow(generator_instance).to receive(:call).and_return(1) - described_class.new.perform(user.id, start_at: start_at, end_at: end_at, mode: mode) expect(Tracks::Generator).to have_received(:new).with( @@ -73,72 +69,6 @@ RSpec.describe Tracks::CreateJob, type: :job do end end - context 'with mode translation' do - before do - allow(Tracks::Generator).to receive(:new).and_return(generator_instance) - allow(generator_instance).to receive(:call) # No tracks created for mode tests - allow(Notifications::Create).to receive(:new).and_return(notification_service) - allow(notification_service).to receive(:call) - end - - it 'translates :none to :incremental' do - allow(generator_instance).to receive(:call).and_return(0) - - described_class.new.perform(user.id, mode: :none) - - expect(Tracks::Generator).to have_received(:new).with( - user, - start_at: nil, - end_at: nil, - mode: :incremental - ) - expect(Notifications::Create).to have_received(:new).with( - user: user, - kind: :info, - title: 'Tracks Generated', - content: 'Created 0 tracks from your location data. Check your tracks section to view them.' - ) - end - - it 'translates :daily to :daily' do - allow(generator_instance).to receive(:call).and_return(0) - - described_class.new.perform(user.id, mode: :daily) - - expect(Tracks::Generator).to have_received(:new).with( - user, - start_at: nil, - end_at: nil, - mode: :daily - ) - expect(Notifications::Create).to have_received(:new).with( - user: user, - kind: :info, - title: 'Tracks Generated', - content: 'Created 0 tracks from your location data. Check your tracks section to view them.' - ) - end - - it 'translates other modes to :bulk' do - allow(generator_instance).to receive(:call).and_return(0) - - described_class.new.perform(user.id, mode: :replace) - - expect(Tracks::Generator).to have_received(:new).with( - user, - start_at: nil, - end_at: nil, - mode: :bulk - ) - expect(Notifications::Create).to have_received(:new).with( - user: user, - kind: :info, - title: 'Tracks Generated', - content: 'Created 0 tracks from your location data. Check your tracks section to view them.' - ) - end - end - context 'when generator raises an error' do let(:error_message) { 'Something went wrong' } let(:notification_service) { instance_double(Notifications::Create) } @@ -175,12 +105,13 @@ RSpec.describe Tracks::CreateJob, type: :job do end context 'when user does not exist' do - it 'handles the error gracefully and creates error notification' do + before do allow(User).to receive(:find).with(999).and_raise(ActiveRecord::RecordNotFound) allow(ExceptionReporter).to receive(:call) allow(Notifications::Create).to receive(:new).and_return(instance_double(Notifications::Create, call: nil)) + end - # Should not raise an error because it's caught by the rescue block + it 'handles the error gracefully and creates error notification' do expect { described_class.new.perform(999) }.not_to raise_error expect(ExceptionReporter).to have_received(:call) @@ -188,15 +119,14 @@ RSpec.describe Tracks::CreateJob, type: :job do end context 'when tracks are deleted and recreated' do - it 'returns the correct count of newly created tracks' do - # Create some existing tracks first - create_list(:track, 3, user: user) + let(:existing_tracks) { create_list(:track, 3, user: user) } - # Mock the generator to simulate deleting existing tracks and creating new ones - # This should return the count of newly created tracks, not the difference + before do allow(generator_instance).to receive(:call).and_return(2) + end - described_class.new.perform(user.id, mode: :bulk) + it 'returns the correct count of newly created tracks' do + described_class.new.perform(user.id, mode: :incremental) expect(Tracks::Generator).to have_received(:new).with( user, diff --git a/spec/models/trip_spec.rb b/spec/models/trip_spec.rb index eecf3fb8..20bb5ba3 100644 --- a/spec/models/trip_spec.rb +++ b/spec/models/trip_spec.rb @@ -160,7 +160,7 @@ RSpec.describe Trip, type: :model do end end - describe '#recalculate_distance!' do + describe '#recalculate_distance!' do it 'recalculates and saves the distance' do original_distance = trip.distance diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7275e402..0cd0f177 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -41,9 +41,6 @@ RSpec.configure do |config| config.before(:suite) do Rails.application.reload_routes! - - # DatabaseCleaner.strategy = :transaction - # DatabaseCleaner.clean_with(:truncation) end config.before do @@ -92,12 +89,6 @@ RSpec.configure do |config| config.after(:suite) do Rake::Task['rswag:generate'].invoke end - - # config.around(:each) do |example| - # DatabaseCleaner.cleaning do - # example.run - # end - # end end Shoulda::Matchers.configure do |config| diff --git a/spec/serializers/track_serializer_spec.rb b/spec/serializers/track_serializer_spec.rb index 42f99175..6213e2c9 100644 --- a/spec/serializers/track_serializer_spec.rb +++ b/spec/serializers/track_serializer_spec.rb @@ -5,95 +5,166 @@ require 'rails_helper' RSpec.describe TrackSerializer do describe '#call' do let(:user) { create(:user) } + let(:track) { create(:track, user: user) } + let(:serializer) { described_class.new(track) } - context 'when serializing user tracks with track IDs' do - subject(:serializer) { described_class.new(user, track_ids).call } + subject(:serialized_track) { serializer.call } - let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) } - let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) } - let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) } - let(:track_ids) { [track1.id, track2.id] } + it 'returns a hash with all required attributes' do + expect(serialized_track).to be_a(Hash) + expect(serialized_track.keys).to contain_exactly( + :id, :start_at, :end_at, :distance, :avg_speed, :duration, + :elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path + ) + end - it 'returns an array of serialized tracks' do - expect(serializer).to be_an(Array) - expect(serializer.length).to eq(2) - end + it 'serializes the track ID correctly' do + expect(serialized_track[:id]).to eq(track.id) + end - it 'serializes each track correctly' do - serialized_ids = serializer.map { |track| track[:id] } - expect(serialized_ids).to contain_exactly(track1.id, track2.id) - expect(serialized_ids).not_to include(track3.id) - end + it 'formats start_at as ISO8601 timestamp' do + expect(serialized_track[:start_at]).to eq(track.start_at.iso8601) + expect(serialized_track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + end - it 'formats timestamps as ISO8601 for all tracks' do - serializer.each do |track| - expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) - expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) - end - end + it 'formats end_at as ISO8601 timestamp' do + expect(serialized_track[:end_at]).to eq(track.end_at.iso8601) + expect(serialized_track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + end - it 'includes all required fields for each track' do - serializer.each do |track| - expect(track.keys).to contain_exactly( - :id, :start_at, :end_at, :distance, :avg_speed, :duration, - :elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path - ) - end - end + it 'converts distance to integer' do + expect(serialized_track[:distance]).to eq(track.distance.to_i) + expect(serialized_track[:distance]).to be_a(Integer) + end - it 'handles numeric values correctly' do - serializer.each do |track| - expect(track[:distance]).to be_a(Numeric) - expect(track[:avg_speed]).to be_a(Numeric) - expect(track[:duration]).to be_a(Numeric) - expect(track[:elevation_gain]).to be_a(Numeric) - expect(track[:elevation_loss]).to be_a(Numeric) - expect(track[:elevation_max]).to be_a(Numeric) - expect(track[:elevation_min]).to be_a(Numeric) - end - end + it 'converts avg_speed to float' do + expect(serialized_track[:avg_speed]).to eq(track.avg_speed.to_f) + expect(serialized_track[:avg_speed]).to be_a(Float) + end - it 'orders tracks by start_at in ascending order' do - serialized_tracks = serializer - expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago - expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago + it 'serializes duration as numeric value' do + expect(serialized_track[:duration]).to eq(track.duration) + expect(serialized_track[:duration]).to be_a(Numeric) + end + + it 'serializes elevation_gain as numeric value' do + expect(serialized_track[:elevation_gain]).to eq(track.elevation_gain) + expect(serialized_track[:elevation_gain]).to be_a(Numeric) + end + + it 'serializes elevation_loss as numeric value' do + expect(serialized_track[:elevation_loss]).to eq(track.elevation_loss) + expect(serialized_track[:elevation_loss]).to be_a(Numeric) + end + + it 'serializes elevation_max as numeric value' do + expect(serialized_track[:elevation_max]).to eq(track.elevation_max) + expect(serialized_track[:elevation_max]).to be_a(Numeric) + end + + it 'serializes elevation_min as numeric value' do + expect(serialized_track[:elevation_min]).to eq(track.elevation_min) + expect(serialized_track[:elevation_min]).to be_a(Numeric) + end + + it 'converts original_path to string' do + expect(serialized_track[:original_path]).to eq(track.original_path.to_s) + expect(serialized_track[:original_path]).to be_a(String) + end + + context 'with decimal distance values' do + let(:track) { create(:track, user: user, distance: 1234.56) } + + it 'truncates distance to integer' do + expect(serialized_track[:distance]).to eq(1234) end end - context 'when track IDs belong to different users' do - subject(:serializer) { described_class.new(user, track_ids).call } + context 'with decimal avg_speed values' do + let(:track) { create(:track, user: user, avg_speed: 25.75) } - let(:other_user) { create(:user) } - let!(:user_track) { create(:track, user: user) } - let!(:other_user_track) { create(:track, user: other_user) } - let(:track_ids) { [user_track.id, other_user_track.id] } - - it 'only returns tracks belonging to the specified user' do - serialized_ids = serializer.map { |track| track[:id] } - expect(serialized_ids).to contain_exactly(user_track.id) - expect(serialized_ids).not_to include(other_user_track.id) + it 'converts avg_speed to float' do + expect(serialized_track[:avg_speed]).to eq(25.75) end end - context 'when track IDs array is empty' do - subject(:serializer) { described_class.new(user, []).call } + context 'with different original_path formats' do + let(:track) { create(:track, user: user, original_path: 'LINESTRING(0 0, 1 1, 2 2)') } - it 'returns an empty array' do - expect(serializer).to eq([]) + it 'converts geometry to WKT string format' do + expect(serialized_track[:original_path]).to eq('LINESTRING (0 0, 1 1, 2 2)') + expect(serialized_track[:original_path]).to be_a(String) end end - context 'when track IDs contain non-existent IDs' do - subject(:serializer) { described_class.new(user, track_ids).call } + context 'with zero values' do + let(:track) do + create(:track, user: user, + distance: 0, + avg_speed: 0.0, + duration: 0, + elevation_gain: 0, + elevation_loss: 0, + elevation_max: 0, + elevation_min: 0) + end - let!(:existing_track) { create(:track, user: user) } - let(:track_ids) { [existing_track.id, 999999] } + it 'handles zero values correctly' do + expect(serialized_track[:distance]).to eq(0) + expect(serialized_track[:avg_speed]).to eq(0.0) + expect(serialized_track[:duration]).to eq(0) + expect(serialized_track[:elevation_gain]).to eq(0) + expect(serialized_track[:elevation_loss]).to eq(0) + expect(serialized_track[:elevation_max]).to eq(0) + expect(serialized_track[:elevation_min]).to eq(0) + end + end - it 'only returns existing tracks' do - serialized_ids = serializer.map { |track| track[:id] } - expect(serialized_ids).to contain_exactly(existing_track.id) - expect(serializer.length).to eq(1) + context 'with very large values' do + let(:track) do + create(:track, user: user, + distance: 1_000_000.0, + avg_speed: 999.99, + duration: 86_400, # 24 hours in seconds + elevation_gain: 10_000, + elevation_loss: 8_000, + elevation_max: 5_000, + elevation_min: 0) + end + + it 'handles large values correctly' do + expect(serialized_track[:distance]).to eq(1_000_000) + expect(serialized_track[:avg_speed]).to eq(999.99) + expect(serialized_track[:duration]).to eq(86_400) + expect(serialized_track[:elevation_gain]).to eq(10_000) + expect(serialized_track[:elevation_loss]).to eq(8_000) + expect(serialized_track[:elevation_max]).to eq(5_000) + expect(serialized_track[:elevation_min]).to eq(0) + end + end + + context 'with different timestamp formats' do + let(:start_time) { Time.current } + let(:end_time) { start_time + 1.hour } + let(:track) { create(:track, user: user, start_at: start_time, end_at: end_time) } + + it 'formats timestamps consistently' do + expect(serialized_track[:start_at]).to eq(start_time.iso8601) + expect(serialized_track[:end_at]).to eq(end_time.iso8601) end end end + + describe '#initialize' do + let(:track) { create(:track) } + + it 'accepts a track parameter' do + expect { described_class.new(track) }.not_to raise_error + end + + it 'stores the track instance' do + serializer = described_class.new(track) + expect(serializer.instance_variable_get(:@track)).to eq(track) + end + end end diff --git a/spec/serializers/tracks_serializer_spec.rb b/spec/serializers/tracks_serializer_spec.rb new file mode 100644 index 00000000..a4a536b7 --- /dev/null +++ b/spec/serializers/tracks_serializer_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TracksSerializer do + describe '#call' do + let(:user) { create(:user) } + + context 'when serializing user tracks with track IDs' do + subject(:serializer) { described_class.new(user, track_ids).call } + + let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) } + let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) } + let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) } + let(:track_ids) { [track1.id, track2.id] } + + it 'returns an array of serialized tracks' do + expect(serializer).to be_an(Array) + expect(serializer.length).to eq(2) + end + + it 'serializes each track correctly' do + serialized_ids = serializer.map { |track| track[:id] } + expect(serialized_ids).to contain_exactly(track1.id, track2.id) + expect(serialized_ids).not_to include(track3.id) + end + + it 'formats timestamps as ISO8601 for all tracks' do + serializer.each do |track| + expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + end + end + + it 'includes all required fields for each track' do + serializer.each do |track| + expect(track.keys).to contain_exactly( + :id, :start_at, :end_at, :distance, :avg_speed, :duration, + :elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path + ) + end + end + + it 'handles numeric values correctly' do + serializer.each do |track| + expect(track[:distance]).to be_a(Numeric) + expect(track[:avg_speed]).to be_a(Numeric) + expect(track[:duration]).to be_a(Numeric) + expect(track[:elevation_gain]).to be_a(Numeric) + expect(track[:elevation_loss]).to be_a(Numeric) + expect(track[:elevation_max]).to be_a(Numeric) + expect(track[:elevation_min]).to be_a(Numeric) + end + end + + it 'orders tracks by start_at in ascending order' do + serialized_tracks = serializer + expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago + expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago + end + end + + context 'when track IDs belong to different users' do + subject(:serializer) { described_class.new(user, track_ids).call } + + let(:other_user) { create(:user) } + let!(:user_track) { create(:track, user: user) } + let!(:other_user_track) { create(:track, user: other_user) } + let(:track_ids) { [user_track.id, other_user_track.id] } + + it 'only returns tracks belonging to the specified user' do + serialized_ids = serializer.map { |track| track[:id] } + expect(serialized_ids).to contain_exactly(user_track.id) + expect(serialized_ids).not_to include(other_user_track.id) + end + end + + context 'when track IDs array is empty' do + subject(:serializer) { described_class.new(user, []).call } + + it 'returns an empty array' do + expect(serializer).to eq([]) + end + end + + context 'when track IDs contain non-existent IDs' do + subject(:serializer) { described_class.new(user, track_ids).call } + + let!(:existing_track) { create(:track, user: user) } + let(:track_ids) { [existing_track.id, 999999] } + + it 'only returns existing tracks' do + serialized_ids = serializer.map { |track| track[:id] } + expect(serialized_ids).to contain_exactly(existing_track.id) + expect(serializer.length).to eq(1) + end + end + end +end diff --git a/spec/services/tracks/incremental_processor_spec.rb b/spec/services/tracks/incremental_processor_spec.rb index a2d21bd5..165af52d 100644 --- a/spec/services/tracks/incremental_processor_spec.rb +++ b/spec/services/tracks/incremental_processor_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Tracks::IncrementalProcessor do it 'processes first point' do expect(Tracks::CreateJob).to receive(:perform_later) - .with(user.id, start_at: nil, end_at: nil, mode: :none) + .with(user.id, start_at: nil, end_at: nil, mode: :incremental) processor.call end end @@ -47,7 +47,7 @@ RSpec.describe Tracks::IncrementalProcessor do it 'processes when time threshold exceeded' do expect(Tracks::CreateJob).to receive(:perform_later) - .with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :none) + .with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental) processor.call end end @@ -65,7 +65,7 @@ RSpec.describe Tracks::IncrementalProcessor do it 'uses existing track end time as start_at' do expect(Tracks::CreateJob).to receive(:perform_later) - .with(user.id, start_at: existing_track.end_at, end_at: Time.zone.at(previous_point.timestamp), mode: :none) + .with(user.id, start_at: existing_track.end_at, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental) processor.call end end @@ -88,7 +88,7 @@ RSpec.describe Tracks::IncrementalProcessor do it 'processes when distance threshold exceeded' do expect(Tracks::CreateJob).to receive(:perform_later) - .with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :none) + .with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental) processor.call end end diff --git a/spec/services/visits/suggest_spec.rb b/spec/services/visits/suggest_spec.rb index be56338c..4a6df048 100644 --- a/spec/services/visits/suggest_spec.rb +++ b/spec/services/visits/suggest_spec.rb @@ -75,7 +75,6 @@ RSpec.describe Visits::Suggest do end context 'when reverse geocoding is enabled' do - # Use a different time range to avoid interference with main tests let(:reverse_geocoding_start_at) { Time.zone.local(2020, 6, 1, 0, 0, 0) } let(:reverse_geocoding_end_at) { Time.zone.local(2020, 6, 1, 2, 0, 0) } diff --git a/spec/support/redis.rb b/spec/support/redis.rb index 6ffa0528..d473b269 100644 --- a/spec/support/redis.rb +++ b/spec/support/redis.rb @@ -2,7 +2,6 @@ RSpec.configure do |config| config.before(:each) do - # Clear the cache before each test Rails.cache.clear end end