Merge pull request #6 from Freika/basic_tests

Basic tests
This commit is contained in:
Eugene Burmakin 2024-04-02 23:34:07 +02:00 committed by GitHub
commit def0eba421
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 540 additions and 72 deletions

View file

@ -1 +1,4 @@
require: rubocop-rails
Style/Documentation:
Enabled: false

View file

@ -15,10 +15,11 @@ gem 'stimulus-rails'
gem 'tailwindcss-rails'
gem 'turbo-rails'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem "importmap-rails"
gem "chartkick"
gem 'importmap-rails'
gem 'chartkick'
gem 'geocoder'
gem 'sidekiq'
gem 'sidekiq-cron'
group :development, :test do
@ -42,4 +43,4 @@ group :development do
end
# Use Redis for Action Cable
gem "redis"
gem 'redis'

View file

@ -106,6 +106,8 @@ GEM
railties (>= 6.1)
drb (2.2.1)
erubi (1.12.0)
et-orbi (1.2.11)
tzinfo
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
@ -113,6 +115,9 @@ GEM
railties (>= 5.0.0)
ffaker (2.23.0)
foreman (0.87.2)
fugit (1.10.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
geocoder (1.8.2)
globalid (1.2.1)
activesupport (>= 6.1)
@ -178,6 +183,7 @@ GEM
nio4r (~> 2.0)
pundit (2.3.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.3)
rack (3.0.10)
rack-session (2.0.0)
@ -274,6 +280,10 @@ GEM
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.19.0)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
globalid (>= 1.0.1)
sidekiq (>= 6)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@ -343,6 +353,7 @@ DEPENDENCIES
rubocop-rails
shoulda-matchers
sidekiq
sidekiq-cron
simplecov
sprockets-rails
stimulus-rails

View file

@ -1,3 +1,3 @@
web: bin/rails server -p 3000 -b 0.0.0.0
css: bin/rails tailwindcss:watch
worker: bundle exec sidekiq
worker: bundle exec sidekiq -C config/sidekiq.yml

View file

@ -2,15 +2,9 @@ class Api::V1::PointsController < ApplicationController
skip_forgery_protection
def create
parsed_params = OwnTracks::Params.new(point_params).call
PointCreatingJob.perform_later(point_params)
@point = Point.create(parsed_params)
if @point.valid?
render json: @point, status: :ok
else
render json: @point.errors, status: :unprocessable_entity
end
render json: {}, status: :ok
end
def destroy

View file

@ -11,6 +11,7 @@ class PointsController < ApplicationController
@distance = distance
@start_at = Time.zone.at(start_at)
@end_at = Time.zone.at(end_at)
@years = (@start_at.year..@end_at.year).to_a
end
private
@ -18,13 +19,13 @@ class PointsController < ApplicationController
def start_at
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
params[:start_at].to_datetime.to_i
Time.parse(params[:start_at]).to_i
end
def end_at
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
params[:end_at].to_datetime.to_i
Time.parse(params[:end_at]).to_i
end
def distance

View file

@ -17,10 +17,18 @@ module ApplicationHelper
end
def year_timespan(year)
start_at = DateTime.new(year).beginning_of_year.to_time.strftime('%Y-%m-%dT%H:%M')
end_at = DateTime.new(year).end_of_year.to_time.strftime('%Y-%m-%dT%H:%M')
start_at = Time.utc(year).in_time_zone('Europe/Berlin').beginning_of_year.strftime('%Y-%m-%dT%H:%M')
end_at = Time.utc(year).in_time_zone('Europe/Berlin').end_of_year.strftime('%Y-%m-%dT%H:%M')
{ start_at: start_at, end_at: end_at }
{ start_at:, end_at: }
end
def timespan(month, year)
month = DateTime.new(year, month).in_time_zone(Time.zone)
start_at = month.beginning_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
end_at = month.end_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
{ start_at:, end_at: }
end
def header_colors
@ -38,4 +46,14 @@ module ApplicationHelper
def year_distance_stat_in_km(year)
Stat.year_distance(year).sum { _1[1] }
end
def is_past?(year, month)
DateTime.new(year, month).past?
end
def points_exist?(year, month)
Point.where(
timestamp: DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
).exists?
end
end

View file

@ -0,0 +1,9 @@
class PointCreatingJob < ApplicationJob
queue_as :default
def perform(point_params)
parsed_params = OwnTracks::Params.new(point_params).call
point = Point.create(parsed_params)
end
end

View file

@ -1,7 +1,9 @@
# frozen_string_literal: true
class StatCreatingJob < ApplicationJob
queue_as :default
def perform(user_id)
CreateStats.new(user_id).call
def perform(user_ids = nil)
CreateStats.new(user_ids).call
end
end

View file

@ -5,20 +5,16 @@ class Stat < ApplicationRecord
belongs_to :user
def timespan
DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
end
def distance_by_day
timespan.to_a.map.with_index(1) do |day, index|
beginning_of_day = day.beginning_of_day.to_i
end_of_day = day.end_of_day.to_i
data = { day: index, distance: 0 }
# We have to filter by user as well
points = Point.where(timestamp: beginning_of_day..end_of_day)
data = { day: index, distance: 0 }
points.each_cons(2) do |point1, point2|
distance = Geocoder::Calculations.distance_between(
[point1.latitude, point1.longitude], [point2.latitude, point2.longitude]
@ -49,6 +45,18 @@ class Stat < ApplicationRecord
data = CountriesAndCities.new(points).call
{ countries: data.count, cities: data.sum { |country| country[:cities].count } }
{ countries: data.map { _1[:country] }.uniq.count, cities: data.sum { |country| country[:cities].count } }
end
def self.years
starting_year = select(:year).min&.year || Time.current.year
(starting_year..Time.current.year).to_a.reverse
end
private
def timespan
DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
end
end

View file

@ -37,8 +37,10 @@ class CountriesAndCities
end
end
def filter_cities(mapped_with_cities)
# In future, we would want to remove cities where user spent less than
# 1 hour per day
# Remove cities with less than MINIMUM_POINTS_IN_CITY
mapped_with_cities.transform_values do |cities|
cities.reject { |_, data| data[:points] < MINIMUM_POINTS_IN_CITY }
@ -48,8 +50,8 @@ class CountriesAndCities
def normalize_result(hash)
hash.map do |country, cities|
{
country: country,
cities: cities.map { |city, data| { city: city, points: data[:points], timestamp: data[:timestamp] } }
country:,
cities: cities.map { |city, data| { city:, points: data[:points], timestamp: data[:timestamp] } }
}
end
end

View file

@ -1,32 +1,34 @@
# frozen_string_literal: true
class CreateStats
attr_reader :years, :months, :user
attr_reader :years, :months, :users
def initialize(user_id)
@user = User.find(user_id)
def initialize(user_ids)
@users = User.where(id: user_ids)
@years = (1970..Time.current.year).to_a
@months = (1..12).to_a
end
def call
years.flat_map do |year|
months.map do |month|
beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
users.each do |user|
years.each do |year|
months.each do |month|
beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
next if points.empty?
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
next if points.empty?
stat = Stat.find_or_initialize_by(year: year, month: month, user: user)
stat.distance = distance(points)
stat.toponyms = toponyms(points)
stat.daily_distance = stat.distance_by_day
stat.save
stat = Stat.find_or_initialize_by(year: year, month: month, user: user)
stat.distance = distance(points)
stat.toponyms = toponyms(points)
stat.daily_distance = stat.distance_by_day
stat.save
stat
stat
end
end
end.compact
end
end
private

View file

@ -6,7 +6,7 @@
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.7.2/dist/full.css" rel="stylesheet" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.9.0/dist/full.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>

View file

@ -1,4 +1,4 @@
<div class='w-3/4 mt-10'>
<div class='w-4/5 mt-10'>
<div class="flex flex-col space-y-4 mb-4 w-full">
<%= form_with url: points_path, method: :get do |f| %>
<div class="flex flex-col md:flex-row md:space-x-4 md:items-end">
@ -32,7 +32,7 @@
</div>
</div>
<div class='w-1/4 mt-10'>
<div class='w-1/5 mt-10'>
<%= render 'shared/right_sidebar' %>
</div>

View file

@ -1,6 +1,36 @@
<%= "#{@distance} km" if @distance %>
<div id='years-nav'>
<div class="dropdown">
<div tabindex="0" role="button" class="btn m-1">Select year</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<% Stat.years.each do |year| %>
<li><%= link_to year, points_url(year_timespan(year).merge(year: year)) %></li>
<% end %>
</ul>
</div>
<% @years.each do |year| %>
<h3 class='text-xl'>
<%= year %>
</h3>
<div class='grid grid-cols-3 gap-3'>
<% (1..12).to_a.each_slice(3) do |months| %>
<% months.each do |month_number| %>
<% if is_past?(year, month_number) && points_exist?(year, month_number) %>
<%= link_to Date::ABBR_MONTHNAMES[month_number], points_url(timespan(month_number, year)), class: 'btn btn-default' %>
<% else %>
<div class='btn btn-disabled'><%= Date::ABBR_MONTHNAMES[month_number] %></div>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% if REVERSE_GEOCODING_ENABLED && @countries_and_cities&.any? %>
<hr class='my-5'>
<% @countries_and_cities.each do |country| %>
<h2 class="text-lg font-semibold mt-5">
<%= country[:country] %> (<%= country[:cities].count %> cities)

View file

@ -48,10 +48,16 @@
<%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %>
<%= link_to '[Map]', points_url(year_timespan(year)), class: 'underline hover:no-underline' %>
</h2>
<p><%= number_with_delimiter year_distance_stat_in_km(year) %>km</p>
<p>
<% cache [current_user, 'year_distance_stat_in_km', year], skip_digest: true do %>
<%= number_with_delimiter year_distance_stat_in_km(year) %>km
<% end %>
</p>
<% if REVERSE_GEOCODING_ENABLED %>
<div class="card-actions justify-end">
<%= countries_and_cities_stat(year) %>
<% cache [current_user, 'countries_and_cities_stat', year], skip_digest: true do %>
<%= countries_and_cities_stat(year) %>
<% end %>
</div>
<% end %>
<%= column_chart(

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
MINIMUM_POINTS_IN_CITY = ENV.fetch('MINIMUM_POINTS_IN_CITY', 5).to_i
MAP_CENTER = ENV.fetch('MAP_CENTER', '[55.7522, 37.6156]')
REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true'

6
config/schedule.yml Normal file
View file

@ -0,0 +1,6 @@
# config/schedule.yml
stat_creating_job:
cron: "0 */6 * * *"
class: "StatCreatingJob"
queue: default

View file

@ -1,6 +1,7 @@
FactoryBot.define do
factory :import do
user_id { "" }
user
name { 'APRIL_2013.json' }
source { 1 }
end
end

View file

@ -21,7 +21,7 @@ FactoryBot.define do
raw_data { "" }
tracker_id { "MyString" }
import_id { "" }
city { "MyString" }
country { "MyString" }
city { nil }
country { nil }
end
end

View file

@ -1,5 +1,22 @@
require 'rails_helper'
RSpec.describe ImportJob, type: :job do
pending "add some examples to (or delete) #{__FILE__}"
describe '#perform' do
subject(:perform) { described_class.new.perform(user.id, import.id) }
let(:file_path) { 'spec/fixtures/owntracks_export.json' }
let(:file) { fixture_file_upload(file_path) }
let(:user) { create(:user) }
let(:import) { create(:import, user: user, file: file, name: File.basename(file.path)) }
it 'creates points' do
expect { perform }.to change { Point.count }.by(8)
end
it 'calls StatCreatingJob' do
expect(StatCreatingJob).to receive(:perform_later).with(user.id)
perform
end
end
end

View file

@ -0,0 +1,15 @@
require 'rails_helper'
RSpec.describe PointCreatingJob, type: :job do
describe '#perform' do
subject(:perform) { described_class.new.perform(point_params) }
let(:point_params) do
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' }
end
it 'creates a point' do
expect { perform }.to change { Point.count }.by(1)
end
end
end

View file

@ -0,0 +1,61 @@
require 'rails_helper'
RSpec.describe ReverseGeocodingJob, type: :job do
describe '#perform' do
subject(:perform) { described_class.new.perform(point.id) }
let(:point) { create(:point) }
before do
allow(Geocoder).to receive(:search).and_return([double(city: 'City', country: 'Country')])
end
context 'when REVERSE_GEOCODING_ENABLED is false' do
before { stub_const('REVERSE_GEOCODING_ENABLED', false) }
it 'does not update point' do
expect { perform }.not_to change { point.reload.city }
end
it 'does not call Geocoder' do
perform
expect(Geocoder).not_to have_received(:search)
end
end
context 'when REVERSE_GEOCODING_ENABLED is true' do
before { stub_const('REVERSE_GEOCODING_ENABLED', true) }
it 'updates point with city and country' do
expect { perform }.to change { point.reload.city }.from(nil)
end
it 'calls Geocoder' do
perform
expect(Geocoder).to have_received(:search).with([point.latitude, point.longitude])
end
context 'when point has city and country' do
let(:point) { create(:point, city: 'City', country: 'Country') }
before do
allow(Geocoder).to receive(:search).and_return(
[double(city: 'Another city', country: 'Some country')]
)
end
it 'does not update point' do
expect { perform }.not_to change { point.reload.city }
end
it 'does not call Geocoder' do
perform
expect(Geocoder).not_to have_received(:search)
end
end
end
end
end

View file

@ -1,5 +1,20 @@
require 'rails_helper'
RSpec.describe StatCreatingJob, type: :job do
pending "add some examples to (or delete) #{__FILE__}"
describe '#perform' do
let(:user) { create(:user) }
subject { described_class.perform_now([user.id]) }
before do
allow(CreateStats).to receive(:new).and_call_original
allow_any_instance_of(CreateStats).to receive(:call)
end
it 'creates a stat' do
subject
expect(CreateStats).to have_received(:new).with([user.id])
end
end
end

View file

@ -1,6 +1,135 @@
require 'rails_helper'
RSpec.describe Stat, type: :model do
it { is_expected.to validate_presence_of(:year) }
it { is_expected.to validate_presence_of(:month) }
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_presence_of(:year) }
it { is_expected.to validate_presence_of(:month) }
end
describe 'methods' do
let(:year) { 2021 }
describe '.year_cities_and_countries' do
subject { described_class.year_cities_and_countries(year) }
before do
stub_const('MINIMUM_POINTS_IN_CITY', 1)
end
context 'when there are points' do
let!(:points) do
create_list(:point, 3, city: 'City', country: 'Country', timestamp: DateTime.new(year, 1))
create_list(:point, 2, city: 'Some City', country: 'Another country', timestamp: DateTime.new(year, 2))
end
it 'returns countries and cities' do
expect(subject).to eq(countries: 2, cities: 2)
end
end
context 'when there are no points' do
it 'returns countries and cities' do
expect(subject).to eq(countries: 0, cities: 0)
end
end
end
describe '.years' do
subject { described_class.years }
context 'when there are no stats' do
it 'returns years' do
expect(subject).to eq([Time.current.year])
end
end
context 'when there are stats' do
let(:user) { create(:user) }
let(:expected_years) { (year..Time.current.year).to_a.reverse }
before do
create(:stat, year: 2021, user: user)
create(:stat, year: 2020, user: user)
end
it 'returns years' do
expect(subject).to eq(expected_years)
end
end
end
describe '#distance_by_day' do
subject { stat.distance_by_day }
let(:user) { create(:user) }
let(:stat) { create(:stat, year: year, month: 1, user: user) }
let(:expected_distance) do
# 31 day of January
(1..31).map { |day| [day, 0] }
end
context 'when there are points' do
let!(:points) do
create(:point, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1))
create(:point, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2))
end
before { expected_distance[0][1] = 157.23 }
it 'returns distance by day' do
expect(subject).to eq(expected_distance)
end
end
context 'when there are no points' do
it 'returns distance by day' do
expect(subject).to eq(expected_distance)
end
end
end
describe '#timespan' do
subject { stat.send(:timespan) }
let(:stat) { build(:stat, year: year, month: 1) }
let(:expected_timespan) { DateTime.new(year, 1).beginning_of_month..DateTime.new(year, 1).end_of_month }
it 'returns timespan' do
expect(subject).to eq(expected_timespan)
end
end
describe '#self.year_distance' do
subject { described_class.year_distance(year) }
let(:user) { create(:user) }
let(:expected_distance) do
(1..12).map { |month| [Date::MONTHNAMES[month], 0] }
end
context 'when there are stats' do
let!(:stats) do
create(:stat, year: year, month: 1, distance: 100, user: user)
create(:stat, year: year, month: 2, distance: 200, user: user)
end
before do
expected_distance[0][1] = 100
expected_distance[1][1] = 200
end
it 'returns year distance' do
expect(subject).to eq(expected_distance)
end
end
context 'when there are no stats' do
it 'returns year distance' do
expect(subject).to eq(expected_distance)
end
end
end
end
end

View file

@ -1,7 +1,68 @@
require 'rails_helper'
RSpec.describe User, type: :model do
it { is_expected.to have_many(:imports).dependent(:destroy) }
it { is_expected.to have_many(:points).through(:imports) }
it { is_expected.to have_many(:stats) }
describe 'associations' do
it { is_expected.to have_many(:imports).dependent(:destroy) }
it { is_expected.to have_many(:points).through(:imports) }
it { is_expected.to have_many(:stats) }
end
describe 'methods' do
let(:user) { create(:user) }
xdescribe '#export_data' do
subject { user.export_data }
let(:import) { create(:import, user: user) }
let(:point) { create(:point, import: import) }
it 'returns json' do
expect(subject).to include(user.email)
expect(subject).to include('dawarich-export')
expect(subject).to include(point.attributes.except('raw_data', 'id', 'created_at', 'updated_at', 'country', 'city', 'import_id').to_json)
end
end
describe '#total_km' do
subject { user.total_km }
let!(:stat_1) { create(:stat, user: user, distance: 10) }
let!(:stat_2) { create(:stat, user: user, distance: 20) }
it 'returns sum of distances' do
expect(subject).to eq(30)
end
end
describe '#total_countries' do
subject { user.total_countries }
let!(:stat) { create(:stat, user: user, toponyms: [{ 'country' => 'Country' }]) }
it 'returns number of countries' do
expect(subject).to eq(1)
end
end
describe '#total_cities' do
subject { user.total_cities }
let!(:stat) { create(:stat, user: user, toponyms: [{ 'city' => 'City' }]) }
it 'returns number of cities' do
expect(subject).to eq(1)
end
end
describe '#total_reverse_geocoded' do
subject { user.total_reverse_geocoded }
let(:import) { create(:import, user: user) }
let!(:point) { create(:point, country: 'Country', city: 'City', import: import) }
it 'returns number of reverse geocoded points' do
expect(subject).to eq(1)
end
end
end
end

View file

@ -12,18 +12,11 @@ RSpec.describe "Api::V1::Points", type: :request do
expect(response).to have_http_status(:success)
end
end
context 'with invalid params' do
let(:params) do
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i }
end
it "returns http unprocessable_entity" do
post api_v1_points_path, params: params
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to eq("{\"topic\":[\"can't be blank\"]}")
it 'enqueues a job' do
expect {
post api_v1_points_path, params: params
}.to have_enqueued_job(PointCreatingJob)
end
end
end

View file

@ -0,0 +1,47 @@
require 'rails_helper'
RSpec.describe CountriesAndCities do
describe '#call' do
subject(:countries_and_cities) { described_class.new(points).call }
let(:points) do
[
create(:point, latitude: 0, longitude: 0, city: 'City', country: 'Country'),
create(:point, latitude: 1, longitude: 1, city: 'City', country: 'Country'),
create(:point, latitude: 2, longitude: 2, city: 'City', country: 'Country'),
create(:point, latitude: 2, longitude: 2, city: 'Another city', country: 'Some Country'),
create(:point, latitude: 2, longitude: 6, city: 'Another city', country: 'Some Country')
]
end
context 'when MINIMUM_POINTS_IN_CITY is 1' do
before do
stub_const('CountriesAndCities::MINIMUM_POINTS_IN_CITY', 1)
end
it 'returns countries and cities' do
expect(countries_and_cities).to eq(
[
{ cities: [{city: "City", points: 3, timestamp: 1}], country: "Country" },
{ cities: [{city: "Another city", points: 2, timestamp: 1}], country: "Some Country" }
]
)
end
end
context 'when MINIMUM_POINTS_IN_CITY is 3' do
before do
stub_const('CountriesAndCities::MINIMUM_POINTS_IN_CITY', 3)
end
it 'returns countries and cities' do
expect(countries_and_cities).to eq(
[
{ cities: [{city: "City", points: 3, timestamp: 1}], country: "Country" },
{ cities: [], country: "Some Country" }
]
)
end
end
end
end

View file

@ -0,0 +1,34 @@
require 'rails_helper'
RSpec.describe CreateStats do
describe '#call' do
subject(:create_stats) { described_class.new(user_ids).call }
let(:user_ids) { [user.id] }
let(:user) { create(:user) }
context 'when there are no points' do
it 'does not create stats' do
expect { create_stats }.not_to change { Stat.count }
end
end
context 'when there are points' do
let!(:import) { create(:import, user: user) }
let!(:point_1) { create(:point, import: import, latitude: 0, longitude: 0) }
let!(:point_2) { create(:point, import: import, latitude: 1, longitude: 2) }
let!(:point_3) { create(:point, import: import, latitude: 3, longitude: 4) }
it 'creates stats' do
expect { create_stats }.to change { Stat.count }.by(1)
end
it 'calculates distance' do
create_stats
expect(Stat.last.distance).to eq(563)
end
end
end
end

View file

@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe OwnTracks::ExportParser do
describe '#call' do
subject(:parser) { described_class.new(import.id).call }
subject(:parser) { described_class.new(import).call }
let(:file_path) { 'spec/fixtures/owntracks_export.json' }
let(:file) { fixture_file_upload(file_path) }