mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
Fix/pre epoch time (#2019)
* Use user timezone to show dates on maps * Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates. * Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates. * Fix tests failing due to new index on stats table * Fix failing specs
This commit is contained in:
parent
9ac4566b5a
commit
516cfabb06
15 changed files with 229 additions and 89 deletions
|
|
@ -12,8 +12,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- Cities visited during a trip are now being calculated correctly. #547
|
- Cities visited during a trip are now being calculated correctly. #547 #641
|
||||||
- Points on the map are now show time in user's timezone. #580
|
- Points on the map are now show time in user's timezone. #580
|
||||||
|
- Date range inputs now handle pre-epoch dates gracefully by clamping to valid PostgreSQL integer range. #685
|
||||||
|
|
||||||
|
|
||||||
# [0.36.2] - 2025-12-06
|
# [0.36.2] - 2025-12-06
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Countries::VisitedCitiesController < ApiController
|
class Api::V1::Countries::VisitedCitiesController < ApiController
|
||||||
|
include SafeTimestampParser
|
||||||
|
|
||||||
before_action :validate_params
|
before_action :validate_params
|
||||||
|
|
||||||
def index
|
def index
|
||||||
start_at = DateTime.parse(params[:start_at]).to_i
|
start_at = safe_timestamp(params[:start_at])
|
||||||
end_at = DateTime.parse(params[:end_at]).to_i
|
end_at = safe_timestamp(params[:end_at])
|
||||||
|
|
||||||
points = current_api_user
|
points = current_api_user
|
||||||
.points
|
.points
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::PointsController < ApiController
|
class Api::V1::PointsController < ApiController
|
||||||
|
include SafeTimestampParser
|
||||||
|
|
||||||
before_action :authenticate_active_api_user!, only: %i[create update destroy bulk_destroy]
|
before_action :authenticate_active_api_user!, only: %i[create update destroy bulk_destroy]
|
||||||
before_action :validate_points_limit, only: %i[create]
|
before_action :validate_points_limit, only: %i[create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
start_at = params[:start_at]&.to_datetime&.to_i
|
start_at = params[:start_at].present? ? safe_timestamp(params[:start_at]) : nil
|
||||||
end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i
|
end_at = params[:end_at].present? ? safe_timestamp(params[:end_at]) : Time.zone.now.to_i
|
||||||
order = params[:order] || 'desc'
|
order = params[:order] || 'desc'
|
||||||
|
|
||||||
points = current_api_user
|
points = current_api_user
|
||||||
|
|
|
||||||
24
app/controllers/concerns/safe_timestamp_parser.rb
Normal file
24
app/controllers/concerns/safe_timestamp_parser.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SafeTimestampParser
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def safe_timestamp(date_string)
|
||||||
|
return Time.zone.now.to_i if date_string.blank?
|
||||||
|
|
||||||
|
parsed_time = Time.zone.parse(date_string)
|
||||||
|
|
||||||
|
# Time.zone.parse returns epoch time (2000-01-01) for unparseable strings
|
||||||
|
# Check if it's a valid parse by seeing if year is suspiciously at epoch
|
||||||
|
return Time.zone.now.to_i if parsed_time.nil? || (parsed_time.year == 2000 && !date_string.include?('2000'))
|
||||||
|
|
||||||
|
min_timestamp = Time.zone.parse('1970-01-01').to_i
|
||||||
|
max_timestamp = Time.zone.parse('2100-01-01').to_i
|
||||||
|
|
||||||
|
parsed_time.to_i.clamp(min_timestamp, max_timestamp)
|
||||||
|
rescue ArgumentError, TypeError
|
||||||
|
Time.zone.now.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Map::LeafletController < ApplicationController
|
class Map::LeafletController < ApplicationController
|
||||||
|
include SafeTimestampParser
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
layout 'map', only: :index
|
layout 'map', only: :index
|
||||||
|
|
||||||
|
|
@ -71,14 +73,14 @@ class Map::LeafletController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def start_at
|
def start_at
|
||||||
return Time.zone.parse(params[:start_at]).to_i if params[:start_at].present?
|
return safe_timestamp(params[:start_at]) if params[:start_at].present?
|
||||||
return Time.zone.at(points.last.timestamp).beginning_of_day.to_i if points.any?
|
return Time.zone.at(points.last.timestamp).beginning_of_day.to_i if points.any?
|
||||||
|
|
||||||
Time.zone.today.beginning_of_day.to_i
|
Time.zone.today.beginning_of_day.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
def end_at
|
def end_at
|
||||||
return Time.zone.parse(params[:end_at]).to_i if params[:end_at].present?
|
return safe_timestamp(params[:end_at]) if params[:end_at].present?
|
||||||
return Time.zone.at(points.last.timestamp).end_of_day.to_i if points.any?
|
return Time.zone.at(points.last.timestamp).end_of_day.to_i if points.any?
|
||||||
|
|
||||||
Time.zone.today.end_of_day.to_i
|
Time.zone.today.end_of_day.to_i
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
module Map
|
module Map
|
||||||
class MaplibreController < ApplicationController
|
class MaplibreController < ApplicationController
|
||||||
|
include SafeTimestampParser
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
layout 'map'
|
layout 'map'
|
||||||
|
|
||||||
|
|
@ -11,13 +13,13 @@ module Map
|
||||||
private
|
private
|
||||||
|
|
||||||
def start_at
|
def start_at
|
||||||
return Time.zone.parse(params[:start_at]).to_i if params[:start_at].present?
|
return safe_timestamp(params[:start_at]) if params[:start_at].present?
|
||||||
|
|
||||||
Time.zone.today.beginning_of_day.to_i
|
Time.zone.today.beginning_of_day.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
def end_at
|
def end_at
|
||||||
return Time.zone.parse(params[:end_at]).to_i if params[:end_at].present?
|
return safe_timestamp(params[:end_at]) if params[:end_at].present?
|
||||||
|
|
||||||
Time.zone.today.end_of_day.to_i
|
Time.zone.today.end_of_day.to_i
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PointsController < ApplicationController
|
class PointsController < ApplicationController
|
||||||
|
include SafeTimestampParser
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -40,13 +42,13 @@ class PointsController < ApplicationController
|
||||||
def start_at
|
def start_at
|
||||||
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
|
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
|
||||||
|
|
||||||
Time.zone.parse(params[:start_at]).to_i
|
safe_timestamp(params[:start_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
def end_at
|
def end_at
|
||||||
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
|
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
|
||||||
|
|
||||||
Time.zone.parse(params[:end_at]).to_i
|
safe_timestamp(params[:end_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
def points
|
def points
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,20 @@
|
||||||
|
|
||||||
module Timestamps
|
module Timestamps
|
||||||
def self.parse_timestamp(timestamp)
|
def self.parse_timestamp(timestamp)
|
||||||
begin
|
min_timestamp = Time.zone.parse('1970-01-01').to_i
|
||||||
# if the timestamp is in ISO 8601 format, try to parse it
|
max_timestamp = Time.zone.parse('2100-01-01').to_i
|
||||||
DateTime.parse(timestamp).to_time.to_i
|
|
||||||
rescue
|
parsed = DateTime.parse(timestamp).to_time.to_i
|
||||||
|
|
||||||
|
parsed.clamp(min_timestamp, max_timestamp)
|
||||||
|
rescue StandardError
|
||||||
|
result =
|
||||||
if timestamp.to_s.length > 10
|
if timestamp.to_s.length > 10
|
||||||
# If the timestamp is in milliseconds, convert to seconds
|
|
||||||
timestamp.to_i / 1000
|
timestamp.to_i / 1000
|
||||||
else
|
else
|
||||||
# If the timestamp is in seconds, return it without change
|
|
||||||
timestamp.to_i
|
timestamp.to_i
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
result.clamp(min_timestamp, max_timestamp)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
101
spec/controllers/concerns/safe_timestamp_parser_spec.rb
Normal file
101
spec/controllers/concerns/safe_timestamp_parser_spec.rb
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SafeTimestampParser, type: :controller do
|
||||||
|
include ActiveSupport::Testing::TimeHelpers
|
||||||
|
|
||||||
|
controller(ActionController::Base) do
|
||||||
|
include SafeTimestampParser
|
||||||
|
|
||||||
|
def index
|
||||||
|
render plain: safe_timestamp(params[:date]).to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
routes.draw { get 'index' => 'anonymous#index' }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#safe_timestamp' do
|
||||||
|
context 'with valid dates within range' do
|
||||||
|
it 'returns correct timestamp for 2020-01-01' do
|
||||||
|
get :index, params: { date: '2020-01-01' }
|
||||||
|
expected = Time.zone.parse('2020-01-01').to_i
|
||||||
|
expect(response.body).to eq(expected.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns correct timestamp for 1980-06-15' do
|
||||||
|
get :index, params: { date: '1980-06-15' }
|
||||||
|
expected = Time.zone.parse('1980-06-15').to_i
|
||||||
|
expect(response.body).to eq(expected.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with dates before valid range' do
|
||||||
|
it 'clamps year 1000 to minimum timestamp (1970-01-01)' do
|
||||||
|
get :index, params: { date: '1000-01-30' }
|
||||||
|
min_timestamp = Time.zone.parse('1970-01-01').to_i
|
||||||
|
expect(response.body).to eq(min_timestamp.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clamps year 1900 to minimum timestamp (1970-01-01)' do
|
||||||
|
get :index, params: { date: '1900-12-25' }
|
||||||
|
min_timestamp = Time.zone.parse('1970-01-01').to_i
|
||||||
|
expect(response.body).to eq(min_timestamp.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clamps year 1969 to minimum timestamp (1970-01-01)' do
|
||||||
|
get :index, params: { date: '1969-07-20' }
|
||||||
|
min_timestamp = Time.zone.parse('1970-01-01').to_i
|
||||||
|
expect(response.body).to eq(min_timestamp.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with dates after valid range' do
|
||||||
|
it 'clamps year 2150 to maximum timestamp (2100-01-01)' do
|
||||||
|
get :index, params: { date: '2150-01-01' }
|
||||||
|
max_timestamp = Time.zone.parse('2100-01-01').to_i
|
||||||
|
expect(response.body).to eq(max_timestamp.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clamps year 3000 to maximum timestamp (2100-01-01)' do
|
||||||
|
get :index, params: { date: '3000-12-31' }
|
||||||
|
max_timestamp = Time.zone.parse('2100-01-01').to_i
|
||||||
|
expect(response.body).to eq(max_timestamp.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid date strings' do
|
||||||
|
it 'returns current time for unparseable date' do
|
||||||
|
travel_to Time.zone.parse('2023-06-15 12:00:00') do
|
||||||
|
get :index, params: { date: 'not-a-date' }
|
||||||
|
expected = Time.zone.now.to_i
|
||||||
|
expect(response.body).to eq(expected.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns current time for empty string' do
|
||||||
|
travel_to Time.zone.parse('2023-06-15 12:00:00') do
|
||||||
|
get :index, params: { date: '' }
|
||||||
|
expected = Time.zone.now.to_i
|
||||||
|
expect(response.body).to eq(expected.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'edge cases' do
|
||||||
|
it 'handles Unix epoch exactly (1970-01-01)' do
|
||||||
|
get :index, params: { date: '1970-01-01' }
|
||||||
|
expected = Time.zone.parse('1970-01-01').to_i
|
||||||
|
expect(response.body).to eq(expected.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles maximum date exactly (2100-01-01)' do
|
||||||
|
get :index, params: { date: '2100-01-01' }
|
||||||
|
expected = Time.zone.parse('2100-01-01').to_i
|
||||||
|
expect(response.body).to eq(expected.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -199,8 +199,8 @@ RSpec.describe User, type: :model do
|
||||||
describe '#total_distance' do
|
describe '#total_distance' do
|
||||||
subject { user.total_distance }
|
subject { user.total_distance }
|
||||||
|
|
||||||
let!(:stat1) { create(:stat, user:, distance: 10_000) }
|
let!(:stat1) { create(:stat, user:, year: 2020, month: 10, distance: 10_000) }
|
||||||
let!(:stat2) { create(:stat, user:, distance: 20_000) }
|
let!(:stat2) { create(:stat, user:, year: 2020, month: 11, distance: 20_000) }
|
||||||
|
|
||||||
it 'returns sum of distances' do
|
it 'returns sum of distances' do
|
||||||
expect(subject).to eq(30) # 30 km
|
expect(subject).to eq(30) # 30 km
|
||||||
|
|
@ -341,14 +341,16 @@ RSpec.describe User, type: :model do
|
||||||
|
|
||||||
describe '.from_omniauth' do
|
describe '.from_omniauth' do
|
||||||
let(:auth_hash) do
|
let(:auth_hash) do
|
||||||
OmniAuth::AuthHash.new({
|
OmniAuth::AuthHash.new(
|
||||||
provider: 'github',
|
{
|
||||||
uid: '123545',
|
provider: 'github',
|
||||||
info: {
|
uid: '123545',
|
||||||
email: email,
|
info: {
|
||||||
name: 'Test User'
|
email: email,
|
||||||
|
name: 'Test User'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user exists with the same email' do
|
context 'when user exists with the same email' do
|
||||||
|
|
@ -394,14 +396,16 @@ RSpec.describe User, type: :model do
|
||||||
context 'when OAuth provider is Google' do
|
context 'when OAuth provider is Google' do
|
||||||
let(:email) { 'google@example.com' }
|
let(:email) { 'google@example.com' }
|
||||||
let(:auth_hash) do
|
let(:auth_hash) do
|
||||||
OmniAuth::AuthHash.new({
|
OmniAuth::AuthHash.new(
|
||||||
provider: 'google_oauth2',
|
{
|
||||||
uid: '123545',
|
provider: 'google_oauth2',
|
||||||
info: {
|
uid: '123545',
|
||||||
email: email,
|
info: {
|
||||||
name: 'Google User'
|
email: email,
|
||||||
|
name: 'Google User'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a user from Google OAuth data' do
|
it 'creates a user from Google OAuth data' do
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ require 'rails_helper'
|
||||||
RSpec.describe 'Api::V1::Stats', type: :request do
|
RSpec.describe 'Api::V1::Stats', type: :request do
|
||||||
describe 'GET /index' do
|
describe 'GET /index' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:stats_in_2020) { create_list(:stat, 12, year: 2020, user:) }
|
let(:stats_in_2020) { (1..12).map { |month| create(:stat, year: 2020, month:, user:) } }
|
||||||
let(:stats_in_2021) { create_list(:stat, 12, year: 2021, user:) }
|
let(:stats_in_2021) { (1..12).map { |month| create(:stat, year: 2021, month:, user:) } }
|
||||||
let(:points_in_2020) do
|
let(:points_in_2020) do
|
||||||
(1..85).map do |i|
|
(1..85).map do |i|
|
||||||
create(:point, :with_geodata,
|
create(:point, :with_geodata,
|
||||||
|
|
@ -50,17 +50,17 @@ RSpec.describe 'Api::V1::Stats', type: :request do
|
||||||
totalCitiesVisited: 1,
|
totalCitiesVisited: 1,
|
||||||
monthlyDistanceKm: {
|
monthlyDistanceKm: {
|
||||||
january: 1,
|
january: 1,
|
||||||
february: 0,
|
february: 1,
|
||||||
march: 0,
|
march: 1,
|
||||||
april: 0,
|
april: 1,
|
||||||
may: 0,
|
may: 1,
|
||||||
june: 0,
|
june: 1,
|
||||||
july: 0,
|
july: 1,
|
||||||
august: 0,
|
august: 1,
|
||||||
september: 0,
|
september: 1,
|
||||||
october: 0,
|
october: 1,
|
||||||
november: 0,
|
november: 1,
|
||||||
december: 0
|
december: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -70,17 +70,17 @@ RSpec.describe 'Api::V1::Stats', type: :request do
|
||||||
totalCitiesVisited: 1,
|
totalCitiesVisited: 1,
|
||||||
monthlyDistanceKm: {
|
monthlyDistanceKm: {
|
||||||
january: 1,
|
january: 1,
|
||||||
february: 0,
|
february: 1,
|
||||||
march: 0,
|
march: 1,
|
||||||
april: 0,
|
april: 1,
|
||||||
may: 0,
|
may: 1,
|
||||||
june: 0,
|
june: 1,
|
||||||
july: 0,
|
july: 1,
|
||||||
august: 0,
|
august: 1,
|
||||||
september: 0,
|
september: 1,
|
||||||
october: 0,
|
october: 1,
|
||||||
november: 0,
|
november: 1,
|
||||||
december: 0
|
december: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -100,4 +100,3 @@ RSpec.describe 'Api::V1::Stats', type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ RSpec.describe PointSerializer do
|
||||||
'track_id' => point.track_id,
|
'track_id' => point.track_id,
|
||||||
'country_name' => point.read_attribute(:country_name),
|
'country_name' => point.read_attribute(:country_name),
|
||||||
'raw_data_archived' => point.raw_data_archived,
|
'raw_data_archived' => point.raw_data_archived,
|
||||||
'raw_data_archive_id' => point.raw_data_archive_id,
|
'raw_data_archive_id' => point.raw_data_archive_id
|
||||||
'timestamp_year' => point.timestamp_year,
|
|
||||||
'timestamp_month' => point.timestamp_month
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ RSpec.describe StatsSerializer do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user has stats' do
|
context 'when the user has stats' do
|
||||||
let!(:stats_in_2020) { create_list(:stat, 12, year: 2020, user:) }
|
let!(:stats_in_2020) { (1..12).map { |month| create(:stat, year: 2020, month:, user:) } }
|
||||||
let!(:stats_in_2021) { create_list(:stat, 12, year: 2021, user:) }
|
let!(:stats_in_2021) { (1..12).map { |month| create(:stat, year: 2021, month:, user:) } }
|
||||||
let!(:points_in_2020) do
|
let!(:points_in_2020) do
|
||||||
(1..85).map do |i|
|
(1..85).map do |i|
|
||||||
create(:point, :with_geodata,
|
create(:point, :with_geodata,
|
||||||
|
|
@ -63,17 +63,17 @@ RSpec.describe StatsSerializer do
|
||||||
"totalCitiesVisited": 1,
|
"totalCitiesVisited": 1,
|
||||||
"monthlyDistanceKm": {
|
"monthlyDistanceKm": {
|
||||||
"january": 1,
|
"january": 1,
|
||||||
"february": 0,
|
"february": 1,
|
||||||
"march": 0,
|
"march": 1,
|
||||||
"april": 0,
|
"april": 1,
|
||||||
"may": 0,
|
"may": 1,
|
||||||
"june": 0,
|
"june": 1,
|
||||||
"july": 0,
|
"july": 1,
|
||||||
"august": 0,
|
"august": 1,
|
||||||
"september": 0,
|
"september": 1,
|
||||||
"october": 0,
|
"october": 1,
|
||||||
"november": 0,
|
"november": 1,
|
||||||
"december": 0
|
"december": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -83,17 +83,17 @@ RSpec.describe StatsSerializer do
|
||||||
"totalCitiesVisited": 1,
|
"totalCitiesVisited": 1,
|
||||||
"monthlyDistanceKm": {
|
"monthlyDistanceKm": {
|
||||||
"january": 1,
|
"january": 1,
|
||||||
"february": 0,
|
"february": 1,
|
||||||
"march": 0,
|
"march": 1,
|
||||||
"april": 0,
|
"april": 1,
|
||||||
"may": 0,
|
"may": 1,
|
||||||
"june": 0,
|
"june": 1,
|
||||||
"july": 0,
|
"july": 1,
|
||||||
"august": 0,
|
"august": 1,
|
||||||
"september": 0,
|
"september": 1,
|
||||||
"october": 0,
|
"october": 1,
|
||||||
"november": 0,
|
"november": 1,
|
||||||
"december": 0
|
"december": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ describe 'Stats API', type: :request do
|
||||||
]
|
]
|
||||||
|
|
||||||
let!(:user) { create(:user) }
|
let!(:user) { create(:user) }
|
||||||
let!(:stats_in_2020) { create_list(:stat, 12, year: 2020, user:) }
|
let!(:stats_in_2020) { (1..12).map { |month| create(:stat, year: 2020, month:, user:) } }
|
||||||
let!(:stats_in_2021) { create_list(:stat, 12, year: 2021, user:) }
|
let!(:stats_in_2021) { (1..12).map { |month| create(:stat, year: 2021, month:, user:) } }
|
||||||
let!(:points_in_2020) do
|
let!(:points_in_2020) do
|
||||||
(1..85).map do |i|
|
(1..85).map do |i|
|
||||||
create(:point, :with_geodata, :reverse_geocoded, timestamp: Time.zone.local(2020, 1, 1).to_i + i.hours,
|
create(:point, :with_geodata, :reverse_geocoded, timestamp: Time.zone.local(2020, 1, 1).to_i + i.hours,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue