Clean up some mess

This commit is contained in:
Eugene Burmakin 2025-07-20 18:57:53 +02:00
parent 708bca26eb
commit f969d5d3e6
22 changed files with 325 additions and 260 deletions

View file

@ -31,7 +31,8 @@ class MapController < ApplicationController
def build_tracks def build_tracks
track_ids = extract_track_ids track_ids = extract_track_ids
TrackSerializer.new(current_user, track_ids).call
TracksSerializer.new(current_user, track_ids).call
end end
def calculate_distance def calculate_distance

View file

@ -6,20 +6,7 @@ class Tracks::CreateJob < ApplicationJob
def perform(user_id, start_at: nil, end_at: nil, mode: :daily) def perform(user_id, start_at: nil, end_at: nil, mode: :daily)
user = User.find(user_id) user = User.find(user_id)
# Translate mode parameter to Generator mode tracks_created = Tracks::Generator.new(user, start_at:, end_at:, mode:).call
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
create_success_notification(user, tracks_created) create_success_notification(user, tracks_created)
rescue StandardError => e rescue StandardError => e

View file

@ -19,7 +19,6 @@
# track.distance # => 5000 (meters stored in DB) # track.distance # => 5000 (meters stored in DB)
# track.distance_in_unit('km') # => 5.0 (converted to km) # track.distance_in_unit('km') # => 5.0 (converted to km)
# track.distance_in_unit('mi') # => 3.11 (converted to miles) # track.distance_in_unit('mi') # => 3.11 (converted to miles)
# track.formatted_distance('km') # => "5.0 km"
# #
module DistanceConvertible module DistanceConvertible
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -38,21 +37,11 @@ module DistanceConvertible
distance.to_f / conversion_factor distance.to_f / conversion_factor
end end
def formatted_distance(unit, precision: 2)
converted_distance = distance_in_unit(unit)
"#{converted_distance.round(precision)} #{unit}"
end
def distance_for_user(user) def distance_for_user(user)
user_unit = user.safe_settings.distance_unit user_unit = user.safe_settings.distance_unit
distance_in_unit(user_unit) distance_in_unit(user_unit)
end 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 module ClassMethods
def convert_distance(distance_meters, unit) def convert_distance(distance_meters, unit)
return 0.0 unless distance_meters.present? return 0.0 unless distance_meters.present?
@ -66,10 +55,5 @@ module DistanceConvertible
distance_meters.to_f / conversion_factor distance_meters.to_f / conversion_factor
end end
def format_distance(distance_meters, unit, precision: 2)
converted = convert_distance(distance_meters, unit)
"#{converted.round(precision)} #{unit}"
end
end end
end end

View file

@ -34,7 +34,7 @@ class Point < ApplicationRecord
after_create :set_country after_create :set_country
after_create_commit :broadcast_coordinates after_create_commit :broadcast_coordinates
after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? } 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 def self.without_raw_data
select(column_names - ['raw_data']) select(column_names - ['raw_data'])
@ -99,8 +99,6 @@ class Point < ApplicationRecord
end end
def recalculate_track def recalculate_track
return unless track.present?
track.recalculate_path_and_distance! track.recalculate_path_and_distance!
end end

View file

@ -1,38 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
class TrackSerializer class TrackSerializer
def initialize(user, track_ids) def initialize(track)
@user = user @track = track
@track_ids = track_ids
end end
def call 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, id: @track.id,
start_at: track.start_at.iso8601, start_at: @track.start_at.iso8601,
end_at: track.end_at.iso8601, end_at: @track.end_at.iso8601,
distance: track.distance.to_i, distance: @track.distance.to_i,
avg_speed: track.avg_speed.to_f, avg_speed: @track.avg_speed.to_f,
duration: track.duration, duration: @track.duration,
elevation_gain: track.elevation_gain, elevation_gain: @track.elevation_gain,
elevation_loss: track.elevation_loss, elevation_loss: @track.elevation_loss,
elevation_max: track.elevation_max, elevation_max: @track.elevation_max,
elevation_min: track.elevation_min, elevation_min: @track.elevation_min,
original_path: track.original_path.to_s original_path: @track.original_path.to_s
} }
end end
end end

View file

@ -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

View file

@ -7,7 +7,7 @@ module Places
end end
def call 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? return if geodata.blank?
@ -15,21 +15,29 @@ module Places
return if properties.blank? return if properties.blank?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@place.name = properties['name'] if properties['name'].present? update_place_name(properties, geodata)
@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!
if properties['name'].present? update_visits_name(properties) if properties['name'].present?
@place
.visits
.where(name: Place::DEFAULT_NAME)
.update_all(name: properties['name'])
end
@place place
end end
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
end end

View file

@ -48,6 +48,7 @@ class Tracks::Generator
Rails.logger.debug "Generator: created #{segments.size} segments" Rails.logger.debug "Generator: created #{segments.size} segments"
tracks_created = 0 tracks_created = 0
segments.each do |segment| segments.each do |segment|
track = create_track_from_segment(segment) track = create_track_from_segment(segment)
tracks_created += 1 if track tracks_created += 1 if track
@ -146,10 +147,6 @@ class Tracks::Generator
day.beginning_of_day.to_i..day.end_of_day.to_i day.beginning_of_day.to_i..day.end_of_day.to_i
end end
def incremental_mode?
mode == :incremental
end
def clean_existing_tracks def clean_existing_tracks
case mode case mode
when :bulk then clean_bulk_tracks when :bulk then clean_bulk_tracks

View file

@ -36,12 +36,7 @@ class Tracks::IncrementalProcessor
start_at = find_start_time start_at = find_start_time
end_at = find_end_time end_at = find_end_time
Tracks::CreateJob.perform_later( Tracks::CreateJob.perform_later(user.id, start_at:, end_at:, mode: :incremental)
user.id,
start_at: start_at,
end_at: end_at,
mode: :none
)
end end
private private

View file

@ -77,7 +77,7 @@ module Tracks::Segmentation
return true if time_diff_seconds > time_threshold_seconds return true if time_diff_seconds > time_threshold_seconds
# Check distance threshold - convert km to meters to match frontend logic # 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 distance_meters = distance_km * 1000 # Convert km to meters
return true if distance_meters > distance_threshold_meters return true if distance_meters > distance_threshold_meters
@ -85,7 +85,7 @@ module Tracks::Segmentation
false false
end end
def calculate_distance_kilometers_between_points(point1, point2) def calculate_km_distance_between_points(point1, point2)
lat1, lon1 = point_coordinates(point1) lat1, lon1 = point_coordinates(point1)
lat2, lon2 = point_coordinates(point2) lat2, lon2 = point_coordinates(point2)

View file

@ -113,7 +113,6 @@ class Users::SafeSettings
end end
def distance_unit def distance_unit
# km or mi
settings.dig('maps', 'distance_unit') settings.dig('maps', 'distance_unit')
end end

View file

@ -50,7 +50,7 @@
</div> </div>
</div> </div>
<div class="card bg-base-300 w-96 shadow-xl m-5"> <div class="card bg-base-300 w-96 shadow-xl m-5">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Visits suggestions</h2> <h2 class="card-title">Visits suggestions</h2>
<p>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.</p> <p>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.</p>

View file

@ -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 MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i
DISTANCE_UNITS = { DISTANCE_UNITS = {
km: 1000, # to meters km: 1000, # to meters
mi: 1609.34, # to meters mi: 1609.34, # to meters
m: 1, # already in meters m: 1, # already in meters
ft: 0.3048, # to meters ft: 0.3048, # to meters
yd: 0.9144 # to meters yd: 0.9144 # to meters
}.freeze }.freeze
APP_VERSION = File.read('.app_version').strip APP_VERSION = File.read('.app_version').strip

View file

@ -4,13 +4,13 @@ require 'rails_helper'
RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do
describe '#perform' do describe '#perform' do
let!(:user) { create(:user) } let(:user) { create(:user) }
let!(:area) { create(:area, user: user) } let(:area) { create(:area, user: user) }
it 'calls the AreaVisitsCalculationService' do 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 end
end end

View file

@ -14,12 +14,10 @@ RSpec.describe Tracks::CreateJob, type: :job do
allow(generator_instance).to receive(:call) allow(generator_instance).to receive(:call)
allow(Notifications::Create).to receive(:new).and_return(notification_service) allow(Notifications::Create).to receive(:new).and_return(notification_service)
allow(notification_service).to receive(:call) allow(notification_service).to receive(:call)
allow(generator_instance).to receive(:call).and_return(2)
end end
it 'calls the generator and creates a notification' do 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) described_class.new.perform(user.id)
expect(Tracks::Generator).to have_received(:new).with( 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(generator_instance).to receive(:call)
allow(Notifications::Create).to receive(:new).and_return(notification_service) allow(Notifications::Create).to receive(:new).and_return(notification_service)
allow(notification_service).to receive(:call) allow(notification_service).to receive(:call)
allow(generator_instance).to receive(:call).and_return(1)
end end
it 'passes custom parameters to the generator' do 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) described_class.new.perform(user.id, start_at: start_at, end_at: end_at, mode: mode)
expect(Tracks::Generator).to have_received(:new).with( expect(Tracks::Generator).to have_received(:new).with(
@ -73,72 +69,6 @@ RSpec.describe Tracks::CreateJob, type: :job do
end end
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 context 'when generator raises an error' do
let(:error_message) { 'Something went wrong' } let(:error_message) { 'Something went wrong' }
let(:notification_service) { instance_double(Notifications::Create) } let(:notification_service) { instance_double(Notifications::Create) }
@ -175,12 +105,13 @@ RSpec.describe Tracks::CreateJob, type: :job do
end end
context 'when user does not exist' do 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(User).to receive(:find).with(999).and_raise(ActiveRecord::RecordNotFound)
allow(ExceptionReporter).to receive(:call) allow(ExceptionReporter).to receive(:call)
allow(Notifications::Create).to receive(:new).and_return(instance_double(Notifications::Create, call: nil)) 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 { described_class.new.perform(999) }.not_to raise_error
expect(ExceptionReporter).to have_received(:call) expect(ExceptionReporter).to have_received(:call)
@ -188,15 +119,14 @@ RSpec.describe Tracks::CreateJob, type: :job do
end end
context 'when tracks are deleted and recreated' do context 'when tracks are deleted and recreated' do
it 'returns the correct count of newly created tracks' do let(:existing_tracks) { create_list(:track, 3, user: user) }
# Create some existing tracks first
create_list(:track, 3, user: user)
# Mock the generator to simulate deleting existing tracks and creating new ones before do
# This should return the count of newly created tracks, not the difference
allow(generator_instance).to receive(:call).and_return(2) 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( expect(Tracks::Generator).to have_received(:new).with(
user, user,

View file

@ -160,7 +160,7 @@ RSpec.describe Trip, type: :model do
end end
end end
describe '#recalculate_distance!' do describe '#recalculate_distance!' do
it 'recalculates and saves the distance' do it 'recalculates and saves the distance' do
original_distance = trip.distance original_distance = trip.distance

View file

@ -41,9 +41,6 @@ RSpec.configure do |config|
config.before(:suite) do config.before(:suite) do
Rails.application.reload_routes! Rails.application.reload_routes!
# DatabaseCleaner.strategy = :transaction
# DatabaseCleaner.clean_with(:truncation)
end end
config.before do config.before do
@ -92,12 +89,6 @@ RSpec.configure do |config|
config.after(:suite) do config.after(:suite) do
Rake::Task['rswag:generate'].invoke Rake::Task['rswag:generate'].invoke
end end
# config.around(:each) do |example|
# DatabaseCleaner.cleaning do
# example.run
# end
# end
end end
Shoulda::Matchers.configure do |config| Shoulda::Matchers.configure do |config|

View file

@ -5,95 +5,166 @@ require 'rails_helper'
RSpec.describe TrackSerializer do RSpec.describe TrackSerializer do
describe '#call' do describe '#call' do
let(:user) { create(:user) } 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(:serialized_track) { serializer.call }
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) } it 'returns a hash with all required attributes' do
let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) } expect(serialized_track).to be_a(Hash)
let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) } expect(serialized_track.keys).to contain_exactly(
let(:track_ids) { [track1.id, track2.id] } :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 it 'serializes the track ID correctly' do
expect(serializer).to be_an(Array) expect(serialized_track[:id]).to eq(track.id)
expect(serializer.length).to eq(2) end
end
it 'serializes each track correctly' do it 'formats start_at as ISO8601 timestamp' do
serialized_ids = serializer.map { |track| track[:id] } expect(serialized_track[:start_at]).to eq(track.start_at.iso8601)
expect(serialized_ids).to contain_exactly(track1.id, track2.id) expect(serialized_track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
expect(serialized_ids).not_to include(track3.id) end
end
it 'formats timestamps as ISO8601 for all tracks' do it 'formats end_at as ISO8601 timestamp' do
serializer.each do |track| expect(serialized_track[:end_at]).to eq(track.end_at.iso8601)
expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) expect(serialized_track[:end_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
end
it 'includes all required fields for each track' do it 'converts distance to integer' do
serializer.each do |track| expect(serialized_track[:distance]).to eq(track.distance.to_i)
expect(track.keys).to contain_exactly( expect(serialized_track[:distance]).to be_a(Integer)
:id, :start_at, :end_at, :distance, :avg_speed, :duration, end
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path
)
end
end
it 'handles numeric values correctly' do it 'converts avg_speed to float' do
serializer.each do |track| expect(serialized_track[:avg_speed]).to eq(track.avg_speed.to_f)
expect(track[:distance]).to be_a(Numeric) expect(serialized_track[:avg_speed]).to be_a(Float)
expect(track[:avg_speed]).to be_a(Numeric) end
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 it 'serializes duration as numeric value' do
serialized_tracks = serializer expect(serialized_track[:duration]).to eq(track.duration)
expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago expect(serialized_track[:duration]).to be_a(Numeric)
expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago 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
end end
context 'when track IDs belong to different users' do context 'with decimal avg_speed values' do
subject(:serializer) { described_class.new(user, track_ids).call } let(:track) { create(:track, user: user, avg_speed: 25.75) }
let(:other_user) { create(:user) } it 'converts avg_speed to float' do
let!(:user_track) { create(:track, user: user) } expect(serialized_track[:avg_speed]).to eq(25.75)
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
end end
context 'when track IDs array is empty' do context 'with different original_path formats' do
subject(:serializer) { described_class.new(user, []).call } let(:track) { create(:track, user: user, original_path: 'LINESTRING(0 0, 1 1, 2 2)') }
it 'returns an empty array' do it 'converts geometry to WKT string format' do
expect(serializer).to eq([]) 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
end end
context 'when track IDs contain non-existent IDs' do context 'with zero values' do
subject(:serializer) { described_class.new(user, track_ids).call } 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) } it 'handles zero values correctly' do
let(:track_ids) { [existing_track.id, 999999] } 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 context 'with very large values' do
serialized_ids = serializer.map { |track| track[:id] } let(:track) do
expect(serialized_ids).to contain_exactly(existing_track.id) create(:track, user: user,
expect(serializer.length).to eq(1) 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 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 end

View file

@ -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

View file

@ -30,7 +30,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'processes first point' do it 'processes first point' do
expect(Tracks::CreateJob).to receive(:perform_later) 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 processor.call
end end
end end
@ -47,7 +47,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'processes when time threshold exceeded' do it 'processes when time threshold exceeded' do
expect(Tracks::CreateJob).to receive(:perform_later) 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 processor.call
end end
end end
@ -65,7 +65,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'uses existing track end time as start_at' do it 'uses existing track end time as start_at' do
expect(Tracks::CreateJob).to receive(:perform_later) 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 processor.call
end end
end end
@ -88,7 +88,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'processes when distance threshold exceeded' do it 'processes when distance threshold exceeded' do
expect(Tracks::CreateJob).to receive(:perform_later) 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 processor.call
end end
end end

View file

@ -75,7 +75,6 @@ RSpec.describe Visits::Suggest do
end end
context 'when reverse geocoding is enabled' do 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_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) } let(:reverse_geocoding_end_at) { Time.zone.local(2020, 6, 1, 2, 0, 0) }

View file

@ -2,7 +2,6 @@
RSpec.configure do |config| RSpec.configure do |config|
config.before(:each) do config.before(:each) do
# Clear the cache before each test
Rails.cache.clear Rails.cache.clear
end end
end end