Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,11 @@ Style/MultilineBlockChain:
RSpec/MultipleExpectations:
Enabled: false

RSpec/VerifiedDoubles:
Enabled: false

RSpec/SpecFilePathFormat:
Exclude:
- "spec/strava/version_spec.rb"
- "spec/strava/oauth/client_spec.rb"
- "spec/strava/oauth/config_spec.rb"
- "spec/strava/models/activities/ride_spec.rb"
- "spec/strava/models/activities/swim_spec.rb"
- "spec/strava/models/activities/run_spec.rb"
Enabled: false

RSpec/ExampleLength:
Enabled: false
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* [#105](https://github.com/dblock/strava-ruby-client/pull/105): Adds `total_elevation_gain`, `total_elevation_loss` and formatted helpers to `Strava::Models::Stream`, computed from altitude stream data - [@dblock](https://github.com/dblock).
* [#106](https://github.com/dblock/strava-ruby-client/pull/106): Adds test coverage reporting with [coveralls.io](https://coveralls.io) - [@dblock](https://github.com/dblock).
* [#108](https://github.com/dblock/strava-ruby-client/pull/108): Adds an integration test that verifies the real Strava API endpoint is reachable, run in CI on every push and pull request via `rake spec:integration` - [@dblock](https://github.com/dblock).
* [#107](https://github.com/dblock/strava-ruby-client/pull/107): Fixes `explore_segments` and `star_segment` raising `UncaughtThrowError` instead of `ArgumentError` for missing required arguments - [@dblock](https://github.com/dblock).
* [#107](https://github.com/dblock/strava-ruby-client/pull/107): Fixes `start_date_local` to always derive the timezone offset from the difference between `start_date` and `start_date_local`, since Strava's `timezone` property does not account for daylight saving time - [@dblock](https://github.com/dblock).
* Your contribution here.

### 3.0.0 (2025/10/24)
Expand Down
6 changes: 4 additions & 2 deletions lib/strava/api/endpoints/segments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ module Segments
# @see https://developers.strava.com/docs/reference/#api-Segments-exploreSegments
#
def explore_segments(options = {})
throw ArgumentError.new('Required argument :bounds missing') if options[:bounds].nil?
raise ArgumentError, 'Required argument :bounds missing' if options[:bounds].nil?

bounds = options[:bounds]
bounds = bounds.map(&:to_s).join(',') if bounds.is_a?(Array)
get('segments/explore', options.merge(bounds: bounds))['segments'].map do |row|
Expand Down Expand Up @@ -109,7 +110,8 @@ def segment(id_or_options, options = {})
#
def star_segment(id_or_options, options = {})
id, options = parse_args(id_or_options, options)
throw ArgumentError.new('Required argument :starred missing') if options[:starred].nil?
raise ArgumentError, 'Required argument :starred missing' if options[:starred].nil?

Strava::Models::DetailedSegment.new(put("segments/#{id}/starred", options))
end
end
Expand Down
45 changes: 12 additions & 33 deletions lib/strava/models/mixins/start_date_local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ module Mixins
# Provides local start date/time with timezone handling.
#
# This mixin adds the start_date_local property and handles proper timezone
# conversion. It attempts to use the 'timezone' property if available, or
# calculates the timezone offset from the difference between start_date
# (UTC) and start_date_local.
# conversion. The offset is calculated from the difference between start_date
# (UTC) and start_date_local, which correctly accounts for daylight saving
# time (unlike Strava's 'timezone' property, which only reflects the zone's
# standard GMT offset).
#
# This is particularly important for activities that cross timezone boundaries
# or for displaying times in the athlete's local timezone.
Expand All @@ -28,16 +29,14 @@ module StartDateLocal
#
# Returns the start date/time in the local timezone.
#
# Constructs a Time object with the proper timezone offset based on
# the 'timezone' property or calculated from the difference between
# start_date and start_date_local.
# Constructs a Time object with the offset calculated from the difference
# between start_date and start_date_local.
#
# @return [Time] Start date/time with local timezone
#
def start_date_local
extracted_datetime = self['start_date_local']
# Some Strava objects do not contain a timezone property (e.g., Lap)
timezone_shift = conditional_timezone(extracted_datetime)
timezone_shift = calculate_timezone(extracted_datetime)
::Time.new(extracted_datetime.year,
extracted_datetime.month,
extracted_datetime.day,
Expand All @@ -52,46 +51,26 @@ def start_date_local
#
# Determines the timezone offset for the start date.
#
# If a 'timezone' property exists, parses it to extract the offset.
# Otherwise, calculates the offset from the difference between
# start_date (UTC) and start_date_local.
# Calculates the offset from the difference between start_date (UTC)
# and start_date_local. This is used instead of Strava's 'timezone'
# property, which reflects the zone's standard GMT offset and does
# not account for daylight saving time.
#
# @param extracted_datetime [Time] The parsed start_date_local value
# @return [String] Timezone offset string (e.g., "-05:00")
#
# @api private
#
def conditional_timezone(extracted_datetime)
if key?(:timezone)
if timezone.include?('+')
timezone_shift_string('+')
elsif timezone.include?('-')
timezone_shift_string('-')
else
raise ArgumentError 'No operator of timezone correction detectable!'
end
else
calculate_timezone(extracted_datetime)
end
end

def calculate_timezone(extracted_datetime)
if start_date == extracted_datetime
timezone_diff_shift_string(0, '-')
elsif extracted_datetime < start_date
timezone_diff_shift_string((extracted_datetime - start_date), '-')
elsif extracted_datetime > start_date
timezone_diff_shift_string((extracted_datetime - start_date), '+')
else
raise ArgumentError 'No operator of timezone correction detectable!'
timezone_diff_shift_string((extracted_datetime - start_date), '+')
end
end

def timezone_shift_string(operator)
diff_hours = timezone.split(operator).last.to_i
"#{operator}#{format_int_leading_zero(diff_hours)}:00"
end

def timezone_diff_shift_string(time_diff, operator)
diff_hours = (time_diff.abs / 3600).to_i
"#{operator}#{format_int_leading_zero(diff_hours)}:00"
Expand Down
6 changes: 4 additions & 2 deletions lib/strava/web/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,12 @@ def config
#
def parse_args(id_or_options, options = {})
if id_or_options.is_a?(Hash)
throw ArgumentError.new('Required argument :id missing') if id_or_options[:id].nil?
raise ArgumentError, 'Required argument :id missing' if id_or_options[:id].nil?

[id_or_options[:id], id_or_options.except(:id)]
else
throw ArgumentError.new('Required argument :id missing') if id_or_options.nil?
raise ArgumentError, 'Required argument :id missing' if id_or_options.nil?

[id_or_options, options]
end
end
Expand Down
83 changes: 83 additions & 0 deletions spec/fixtures/strava/client/star_segment.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions spec/strava/api/client/endpoints/activities/activity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,22 @@
expect(split_metric).to be_a Strava::Models::Split
expect(split_metric.distance).to eq 1001.6
expect(split_metric.distance_in_meters).to eq 1001.6
expect(split_metric.distance_in_feet).to eq 3286.089344
expect(split_metric.distance_in_miles).to eq 0.622364192
expect(split_metric.elapsed_time).to eq 314
expect(split_metric.pace_per_kilometer_s).to eq '5m13s/km'
expect(split_metric.pace_per_mile_s).to eq '8m24s/mi'
expect(split_metric.pace_s).to eq '5m13s/km'
expect(split_metric.average_speed_kilometer_per_hour_s).to eq '11.5km/h'
expect(split_metric.average_speed_miles_per_hour_s).to eq '7.1mph'
expect(split_metric.average_speed_meters_per_second).to eq 3.19
expect(split_metric.elevation_difference).to eq 15.6
expect(split_metric.elevation_difference).to eq 15.6
expect(split_metric.elevation_difference_in_feet).to eq 51.181104
expect(split_metric.elevation_difference_in_meters).to eq 15.6
expect(split_metric.elevation_difference_in_meters_s).to eq '15.6m'
expect(split_metric.elevation_difference_in_feet_s).to eq '51.2ft'
expect(split_metric.elevation_difference_s).to eq '15.6m'
expect(split_metric.moving_time).to eq 314
expect(split_metric.split).to eq 1
expect(split_metric.average_speed).to eq 3.19
Expand Down
5 changes: 5 additions & 0 deletions spec/strava/api/client/endpoints/routes/route_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@
expect(route).to be_a Strava::Models::Route
expect(route.id).to eq 16_341_573
expect(route.athlete).to be_a Strava::Models::SummaryAthlete
expect(route.athlete.name).to eq 'Daniel Doubrovkine'
expect(route.name).to eq 'Lower Manhattan Loop'
expect(route.description).to eq 'My usual long run when I am too lazy to go to Central Park.'
expect(route.elevation_gain).to eq 117.25346822039764
expect(route.elevation_gain_s).to eq '117.3m'
expect(route.elevation_gain_in_feet).to eq 384.68986867620936
expect(route.elevation_gain_in_meters).to eq 117.25346822039764
expect(route.elevation_gain_in_meters_s).to eq '117.3m'
expect(route.elevation_gain_in_feet_s).to eq '384.7ft'
expect(route.map).to be_a Strava::Models::Map
expect(route.private).to be false
expect(route.resource_state).to eq 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@
expect(segment.points).to eq '}qa}Eb`~}Pp@FdA^RJt@nARPVHb@Bb@JFLALS^?`@THv@OT?VLNR\\x@FFL?LI' # TODO: polyline
expect(segment.starred).to be false
end

it 'raises an error when :bounds is missing' do
expect { client.explore_segments }.to raise_error ArgumentError, 'Required argument :bounds missing'
end
end
22 changes: 22 additions & 0 deletions spec/strava/api/client/endpoints/segments/star_segment_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'Strava::Api::Client#star_segment', vcr: { cassette_name: 'client/star_segment' } do
include_context 'with API client'
it 'stars a segment' do
segment = client.star_segment(id: 1_109_718, starred: true)
expect(segment).to be_a Strava::Models::DetailedSegment
expect(segment.starred).to be true
end

it 'stars a segment by id' do
segment = client.star_segment(1_109_718, starred: true)
expect(segment).to be_a Strava::Models::DetailedSegment
expect(segment.starred).to be true
end

it 'raises an error when :starred is missing' do
expect { client.star_segment(id: 1_109_718) }.to raise_error ArgumentError, 'Required argument :starred missing'
end
end
23 changes: 23 additions & 0 deletions spec/strava/api/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@

it_behaves_like 'web client'

describe '.configure' do
after do
described_class.config.access_token = nil
end

it 'yields the config when a block is given' do
described_class.configure do |config|
config.access_token = 'token'
end
expect(described_class.config.access_token).to eq 'token'
end

it 'returns the config when no block is given' do
expect(described_class.configure).to eq Strava::Api::Config
end
end

describe '.config' do
it 'returns the config' do
expect(described_class.config).to eq Strava::Api::Config
end
end

context 'with errors' do
it 'handles authorization errors', vcr: { cassette_name: 'client/authorization_error' } do
expect { client.activity(id: 1_946_417_534) }.to raise_error Strava::Errors::Fault, /Authorization Error/
Expand Down
48 changes: 48 additions & 0 deletions spec/strava/api/pagination_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Strava::Api::Pagination do
let(:web_response) { double('web_response', http_response: double('http_response')) }
let(:collection) { %w[a b c] }
let(:pagination) { described_class.new(collection, web_response) }

describe '#collection' do
it 'returns the underlying collection' do
expect(pagination.collection).to eq collection
end
end

describe '#size' do
it 'delegates to the collection' do
expect(pagination.size).to eq 3
end
end

describe '#each' do
it 'iterates over the collection' do
expect { |b| pagination.each(&b) }.to yield_successive_args('a', 'b', 'c')
end

it 'returns the collection when no block is given' do
expect(pagination.each).to eq collection
end
end

describe 'method_missing' do
it 'delegates unknown methods to the collection' do
expect(pagination.first).to eq 'a'
expect(pagination.last).to eq 'c'
end
end

describe 'respond_to?' do
it 'returns true for methods supported by the collection' do
expect(pagination.respond_to?(:first)).to be true
end

it 'returns false for methods not supported by the collection' do
expect(pagination.respond_to?(:not_a_real_method)).to be false
end
end
end
Loading
Loading