dawarich/spec/services/tracks/track_builder_spec.rb
2025-08-01 14:23:59 +02:00

330 lines
9.4 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Tracks::TrackBuilder do
# Create a test class that includes the concern for testing
let(:test_class) do
Class.new do
include Tracks::TrackBuilder
def initialize(user)
@user = user
end
private
attr_reader :user
end
end
let(:user) { create(:user) }
let(:builder) { test_class.new(user) }
before do
# Set up user settings for consistent testing
allow_any_instance_of(Users::SafeSettings).to receive(:distance_unit).and_return('km')
end
describe '#create_track_from_points' do
context 'with valid points' do
let!(:points) do
[
create(:point, user: user, lonlat: 'POINT(-74.0060 40.7128)',
timestamp: 2.hours.ago.to_i, altitude: 100),
create(:point, user: user, lonlat: 'POINT(-74.0070 40.7130)',
timestamp: 1.hour.ago.to_i, altitude: 110),
create(:point, user: user, lonlat: 'POINT(-74.0080 40.7132)',
timestamp: 30.minutes.ago.to_i, altitude: 105)
]
end
let(:pre_calculated_distance) { 1500 } # 1500 meters
it 'creates a track with correct attributes' do
track = builder.create_track_from_points(points, pre_calculated_distance)
expect(track).to be_persisted
expect(track.user).to eq(user)
expect(track.start_at).to be_within(1.second).of(Time.zone.at(points.first.timestamp))
expect(track.end_at).to be_within(1.second).of(Time.zone.at(points.last.timestamp))
expect(track.distance).to eq(1500)
expect(track.duration).to be_within(3.seconds).of(90.minutes.to_i)
expect(track.avg_speed).to be > 0
expect(track.original_path).to be_present
end
it 'calculates elevation statistics correctly' do
track = builder.create_track_from_points(points, pre_calculated_distance)
expect(track.elevation_gain).to eq(10) # 110 - 100
expect(track.elevation_loss).to eq(5) # 110 - 105
expect(track.elevation_max).to eq(110)
expect(track.elevation_min).to eq(100)
end
it 'associates points with the track' do
track = builder.create_track_from_points(points, pre_calculated_distance)
points.each(&:reload)
expect(points.map(&:track)).to all(eq(track))
end
end
context 'with insufficient points' do
let(:single_point) { [create(:point, user: user)] }
it 'returns nil for single point' do
result = builder.create_track_from_points(single_point, 1000)
expect(result).to be_nil
end
it 'returns nil for empty array' do
result = builder.create_track_from_points([], 1000)
expect(result).to be_nil
end
end
context 'when track save fails' do
let(:points) do
[
create(:point, user: user, timestamp: 1.hour.ago.to_i),
create(:point, user: user, timestamp: 30.minutes.ago.to_i)
]
end
before do
allow_any_instance_of(Track).to receive(:save).and_return(false)
end
it 'returns nil and logs error' do
expect(Rails.logger).to receive(:error).with(
/Failed to create track for user #{user.id}/
)
result = builder.create_track_from_points(points, 1000)
expect(result).to be_nil
end
end
end
describe '#build_path' do
let(:points) do
[
create(:point, lonlat: 'POINT(-74.0060 40.7128)'),
create(:point, lonlat: 'POINT(-74.0070 40.7130)')
]
end
it 'builds path using Tracks::BuildPath service' do
expect(Tracks::BuildPath).to receive(:new).with(
points
).and_call_original
result = builder.build_path(points)
expect(result).to be_a(RGeo::Geographic::SphericalLineStringImpl)
end
end
describe '#calculate_track_distance' do
let(:points) do
[
create(:point, user: user, lonlat: 'POINT(-74.0060 40.7128)'),
create(:point, user: user, lonlat: 'POINT(-74.0070 40.7130)')
]
end
before do
# Mock Point.total_distance to return distance in meters
allow(Point).to receive(:total_distance).with(points, :m).and_return(1500) # 1500 meters
end
it 'stores distance in meters regardless of user unit preference' do
result = builder.calculate_track_distance(points)
expect(result).to eq(1500) # Always stored as meters
end
it 'rounds distance to nearest meter' do
allow(Point).to receive(:total_distance).with(points, :m).and_return(1500.7)
result = builder.calculate_track_distance(points)
expect(result).to eq(1501) # Rounded to nearest meter
end
end
describe '#calculate_duration' do
let(:start_time) { 2.hours.ago.to_i }
let(:end_time) { 1.hour.ago.to_i }
let(:points) do
[
double(timestamp: start_time),
double(timestamp: end_time)
]
end
it 'calculates duration in seconds' do
result = builder.calculate_duration(points)
expect(result).to eq(1.hour.to_i)
end
end
describe '#calculate_average_speed' do
context 'with valid distance and duration' do
it 'calculates speed in km/h' do
distance_meters = 1000 # 1 km
duration_seconds = 3600 # 1 hour
result = builder.calculate_average_speed(distance_meters, duration_seconds)
expect(result).to eq(1.0) # 1 km/h
end
it 'rounds to 2 decimal places' do
distance_meters = 1500 # 1.5 km
duration_seconds = 1800 # 30 minutes
result = builder.calculate_average_speed(distance_meters, duration_seconds)
expect(result).to eq(3.0) # 3 km/h
end
end
context 'with invalid inputs' do
it 'returns 0.0 for zero duration' do
result = builder.calculate_average_speed(1000, 0)
expect(result).to eq(0.0)
end
it 'returns 0.0 for zero distance' do
result = builder.calculate_average_speed(0, 3600)
expect(result).to eq(0.0)
end
it 'returns 0.0 for negative duration' do
result = builder.calculate_average_speed(1000, -3600)
expect(result).to eq(0.0)
end
end
end
describe '#calculate_elevation_stats' do
context 'with elevation data' do
let(:points) do
[
double(altitude: 100),
double(altitude: 150),
double(altitude: 120),
double(altitude: 180),
double(altitude: 160)
]
end
it 'calculates elevation gain correctly' do
result = builder.calculate_elevation_stats(points)
expect(result[:gain]).to eq(110) # (150-100) + (180-120) = 50 + 60 = 110
end
it 'calculates elevation loss correctly' do
result = builder.calculate_elevation_stats(points)
expect(result[:loss]).to eq(50) # (150-120) + (180-160) = 30 + 20 = 50
end
it 'finds max elevation' do
result = builder.calculate_elevation_stats(points)
expect(result[:max]).to eq(180)
end
it 'finds min elevation' do
result = builder.calculate_elevation_stats(points)
expect(result[:min]).to eq(100)
end
end
context 'with no elevation data' do
let(:points) do
[
double(altitude: nil),
double(altitude: nil)
]
end
it 'returns default elevation stats' do
result = builder.calculate_elevation_stats(points)
expect(result).to eq({
gain: 0,
loss: 0,
max: 0,
min: 0
})
end
end
context 'with mixed elevation data' do
let(:points) do
[
double(altitude: 100),
double(altitude: nil),
double(altitude: 150)
]
end
it 'ignores nil values' do
result = builder.calculate_elevation_stats(points)
expect(result[:gain]).to eq(50) # 150 - 100
expect(result[:loss]).to eq(0)
expect(result[:max]).to eq(150)
expect(result[:min]).to eq(100)
end
end
end
describe '#default_elevation_stats' do
it 'returns hash with zero values' do
result = builder.default_elevation_stats
expect(result).to eq({
gain: 0,
loss: 0,
max: 0,
min: 0
})
end
end
describe 'user method requirement' do
let(:invalid_class) do
Class.new do
include Tracks::TrackBuilder
# Does not implement user method
end
end
it 'raises NotImplementedError when user method is not implemented' do
invalid_builder = invalid_class.new
expect { invalid_builder.send(:user) }.to raise_error(
NotImplementedError,
"Including class must implement user method"
)
end
end
describe 'integration test' do
let!(:points) do
[
create(:point, user: user, lonlat: 'POINT(-74.0060 40.7128)',
timestamp: 2.hours.ago.to_i, altitude: 100),
create(:point, user: user, lonlat: 'POINT(-74.0070 40.7130)',
timestamp: 1.hour.ago.to_i, altitude: 120)
]
end
let(:pre_calculated_distance) { 2000 }
it 'creates a complete track end-to-end' do
expect { builder.create_track_from_points(points, pre_calculated_distance) }.to change(Track, :count).by(1)
track = Track.last
expect(track.user).to eq(user)
expect(track.points).to match_array(points)
expect(track.distance).to eq(2000)
expect(track.duration).to be_within(1.second).of(1.hour.to_i)
expect(track.elevation_gain).to eq(20)
end
end
end