Clean up specs a bit

This commit is contained in:
Eugene Burmakin 2025-06-26 19:48:42 +02:00
parent 2088b769d7
commit 631ee0e64c
14 changed files with 203 additions and 241 deletions

View file

@ -34,6 +34,7 @@ gem 'rgeo-activerecord'
gem 'rgeo-geojson' gem 'rgeo-geojson'
gem 'rswag-api' gem 'rswag-api'
gem 'rswag-ui' gem 'rswag-ui'
gem 'rubyzip', '~> 2.4'
gem 'sentry-ruby' gem 'sentry-ruby'
gem 'sentry-rails' gem 'sentry-rails'
gem 'stackprof' gem 'stackprof'
@ -76,5 +77,3 @@ group :development do
gem 'foreman' gem 'foreman'
gem 'rubocop-rails', require: false gem 'rubocop-rails', require: false
end end
gem "rubyzip", "~> 2.4"

View file

@ -10,7 +10,7 @@ require 'zip'
# "distance_unit": "km", # "distance_unit": "km",
# "timezone": "UTC", # "timezone": "UTC",
# "immich_url": "https://immich.example.com", # "immich_url": "https://immich.example.com",
# // ... other user settings # // ... other user settings (exported via user.safe_settings.settings)
# }, # },
# "areas": [ # "areas": [
# { # {
@ -18,7 +18,8 @@ require 'zip'
# "latitude": "40.7128", # "latitude": "40.7128",
# "longitude": "-74.0060", # "longitude": "-74.0060",
# "radius": 100, # "radius": 100,
# "created_at": "2024-01-01T00:00:00Z" # "created_at": "2024-01-01T00:00:00Z",
# "updated_at": "2024-01-01T00:00:00Z"
# } # }
# ], # ],
# "imports": [ # "imports": [
@ -26,12 +27,14 @@ require 'zip'
# "name": "2023_MARCH.json", # "name": "2023_MARCH.json",
# "source": "google_semantic_history", # "source": "google_semantic_history",
# "created_at": "2024-01-01T00:00:00Z", # "created_at": "2024-01-01T00:00:00Z",
# "updated_at": "2024-01-01T00:00:00Z",
# "processed": true, # "processed": true,
# "points_count": 1500,
# "file_name": "import_1_2023_MARCH.json", # "file_name": "import_1_2023_MARCH.json",
# "original_filename": "2023_MARCH.json", # "original_filename": "2023_MARCH.json",
# "file_size": 2048576, # "file_size": 2048576,
# "content_type": "application/json" # "content_type": "application/json"
# // Note: file_error may be present if file download fails
# // Note: file_name and original_filename will be null if no file attached
# } # }
# ], # ],
# "exports": [ # "exports": [
@ -40,11 +43,16 @@ require 'zip'
# "status": "completed", # "status": "completed",
# "file_format": "json", # "file_format": "json",
# "file_type": "points", # "file_type": "points",
# "start_at": "2024-01-01T00:00:00Z",
# "end_at": "2024-01-31T23:59:59Z",
# "created_at": "2024-02-01T00:00:00Z", # "created_at": "2024-02-01T00:00:00Z",
# "updated_at": "2024-02-01T00:00:00Z",
# "file_name": "export_1_export_2024-01-01_to_2024-01-31.json", # "file_name": "export_1_export_2024-01-01_to_2024-01-31.json",
# "original_filename": "export_2024-01-01_to_2024-01-31.json", # "original_filename": "export_2024-01-01_to_2024-01-31.json",
# "file_size": 1048576, # "file_size": 1048576,
# "content_type": "application/json" # "content_type": "application/json"
# // Note: file_error may be present if file download fails
# // Note: file_name and original_filename will be null if no file attached
# } # }
# ], # ],
# "trips": [ # "trips": [
@ -53,7 +61,9 @@ require 'zip'
# "started_at": "2024-01-15T08:00:00Z", # "started_at": "2024-01-15T08:00:00Z",
# "ended_at": "2024-01-18T20:00:00Z", # "ended_at": "2024-01-18T20:00:00Z",
# "distance": 1245.67, # "distance": 1245.67,
# "created_at": "2024-01-19T00:00:00Z" # "created_at": "2024-01-19T00:00:00Z",
# "updated_at": "2024-01-19T00:00:00Z"
# // ... other trip fields
# } # }
# ], # ],
# "stats": [ # "stats": [
@ -61,10 +71,12 @@ require 'zip'
# "year": 2024, # "year": 2024,
# "month": 1, # "month": 1,
# "distance": 456.78, # "distance": 456.78,
# "daily_distance": [[1, 15.2], [2, 23.5], ...], // [day, distance] pairs
# "toponyms": [ # "toponyms": [
# {"country": "United States", "cities": [{"city": "New York"}]} # {"country": "United States", "cities": [{"city": "New York"}]}
# ], # ],
# "created_at": "2024-02-01T00:00:00Z" # "created_at": "2024-02-01T00:00:00Z",
# "updated_at": "2024-02-01T00:00:00Z"
# } # }
# ], # ],
# "notifications": [ # "notifications": [
@ -72,8 +84,9 @@ require 'zip'
# "kind": "info", # "kind": "info",
# "title": "Import completed", # "title": "Import completed",
# "content": "Your data import has been processed successfully", # "content": "Your data import has been processed successfully",
# "read": true, # "read_at": "2024-01-01T12:30:00Z", // null if unread
# "created_at": "2024-01-01T12:00:00Z" # "created_at": "2024-01-01T12:00:00Z",
# "updated_at": "2024-01-01T12:30:00Z"
# } # }
# ], # ],
# "points": [ # "points": [
@ -84,7 +97,30 @@ require 'zip'
# "altitude": 15.5, # "altitude": 15.5,
# "velocity": 25.5, # "velocity": 25.5,
# "accuracy": 5.0, # "accuracy": 5.0,
# "ping": "test-ping",
# "tracker_id": "tracker-123",
# "topic": "owntracks/user/device",
# "trigger": "manual_event",
# "bssid": "aa:bb:cc:dd:ee:ff",
# "ssid": "TestWiFi",
# "connection": "wifi",
# "vertical_accuracy": 3.0,
# "mode": 2,
# "inrids": ["region1", "region2"],
# "in_regions": ["home", "work"],
# "raw_data": {"test": "data"},
# "city": "New York",
# "country": "United States",
# "geodata": {"address": "123 Main St"},
# "reverse_geocoded_at": "2024-01-01T00:00:00Z",
# "course": 45.5,
# "course_accuracy": 2.5,
# "external_track_id": "ext-123",
# "lonlat": "POINT(-74.006 40.7128)",
# "longitude": -74.006,
# "latitude": 40.7128,
# "created_at": "2024-01-01T00:00:00Z", # "created_at": "2024-01-01T00:00:00Z",
# "updated_at": "2024-01-01T00:00:00Z",
# "import_reference": { # "import_reference": {
# "name": "2023_MARCH.json", # "name": "2023_MARCH.json",
# "source": "google_semantic_history", # "source": "google_semantic_history",
@ -105,9 +141,15 @@ require 'zip'
# // Example of point without relationships (edge cases) # // Example of point without relationships (edge cases)
# "timestamp": 1704070800, # "timestamp": 1704070800,
# "altitude": 10.0, # "altitude": 10.0,
# "longitude": -73.9857,
# "latitude": 40.7484,
# "lonlat": "POINT(-73.9857 40.7484)",
# "created_at": "2024-01-01T00:05:00Z",
# "updated_at": "2024-01-01T00:05:00Z",
# "import_reference": null, // Orphaned point # "import_reference": null, // Orphaned point
# "country_info": null, // No country data # "country_info": null, // No country data
# "visit_reference": null // Not part of a visit # "visit_reference": null // Not part of a visit
# // ... other point fields may be null
# } # }
# ], # ],
# "visits": [ # "visits": [
@ -117,28 +159,38 @@ require 'zip'
# "ended_at": "2024-01-01T17:00:00Z", # "ended_at": "2024-01-01T17:00:00Z",
# "duration": 32400, # "duration": 32400,
# "status": "suggested", # "status": "suggested",
# "created_at": "2024-01-01T00:00:00Z",
# "updated_at": "2024-01-01T00:00:00Z",
# "place_reference": { # "place_reference": {
# "name": "Office Building", # "name": "Office Building",
# "latitude": "40.7589", # "latitude": "40.7589",
# "longitude": "-73.9851", # "longitude": "-73.9851",
# "source": "manual" # "source": "manual"
# } # }
# // ... other visit fields
# }, # },
# { # {
# // Example of visit without place # // Example of visit without place
# "name": "Unknown Location", # "name": "Unknown Location",
# "started_at": "2024-01-02T10:00:00Z", # "started_at": "2024-01-02T10:00:00Z",
# "ended_at": "2024-01-02T12:00:00Z", # "ended_at": "2024-01-02T12:00:00Z",
# "duration": 7200,
# "status": "confirmed",
# "created_at": "2024-01-02T00:00:00Z",
# "updated_at": "2024-01-02T00:00:00Z",
# "place_reference": null // No associated place # "place_reference": null // No associated place
# } # }
# ], # ],
# "places": [ # "places": [
# { # {
# "name": "Office Building", # "name": "Office Building",
# "lonlat": "POINT(-73.9851 40.7589)", # "longitude": "-73.9851",
# "latitude": "40.7589",
# "source": "manual", # "source": "manual",
# "geodata": {"properties": {"name": "Office Building"}}, # "geodata": {"properties": {"name": "Office Building"}},
# "created_at": "2024-01-01T00:00:00Z" # "created_at": "2024-01-01T00:00:00Z",
# "updated_at": "2024-01-01T00:00:00Z"
# // ... other place fields
# } # }
# ] # ]
# } # }

View file

@ -9,4 +9,8 @@ RSpec.describe Country, type: :model do
it { is_expected.to validate_presence_of(:iso_a3) } it { is_expected.to validate_presence_of(:iso_a3) }
it { is_expected.to validate_presence_of(:geom) } it { is_expected.to validate_presence_of(:geom) }
end end
describe 'associations' do
it { is_expected.to have_many(:points).dependent(:nullify) }
end
end end

View file

@ -10,5 +10,40 @@ RSpec.describe Export, type: :model do
describe 'enums' do describe 'enums' do
it { is_expected.to define_enum_for(:status).with_values(created: 0, processing: 1, completed: 2, failed: 3) } it { is_expected.to define_enum_for(:status).with_values(created: 0, processing: 1, completed: 2, failed: 3) }
it { is_expected.to define_enum_for(:file_format).with_values(json: 0, gpx: 1, archive: 2) } it { is_expected.to define_enum_for(:file_format).with_values(json: 0, gpx: 1, archive: 2) }
it { is_expected.to define_enum_for(:file_type).with_values(points: 0, user_data: 1) }
end
describe 'callbacks' do
describe 'after_commit' do
context 'when the export is created' do
let(:export) { build(:export) }
it 'enqueues the ExportJob' do
expect(ExportJob).to receive(:perform_later).with(export.id)
export.save!
end
context 'when the export is a user data export' do
let(:export) { build(:export, file_type: :user_data) }
it 'does not enqueue the ExportJob' do
expect(ExportJob).not_to receive(:perform_later).with(export.id)
export.save!
end
end
end
context 'when the export is destroyed' do
let(:export) { create(:export) }
it 'removes the attached file' do
expect(export.file).to receive(:purge)
export.destroy!
end
end
end
end end
end end

View file

@ -6,6 +6,8 @@ RSpec.describe Point, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:import).optional } it { is_expected.to belong_to(:import).optional }
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:country).optional }
it { is_expected.to belong_to(:visit).optional }
end end
describe 'validations' do describe 'validations' do

View file

@ -18,24 +18,22 @@ RSpec.describe Users::ExportData::Areas, type: :service do
let!(:area1) { create(:area, user: user, name: 'Home', radius: 100) } let!(:area1) { create(:area, user: user, name: 'Home', radius: 100) }
let!(:area2) { create(:area, user: user, name: 'Work', radius: 200) } let!(:area2) { create(:area, user: user, name: 'Work', radius: 200) }
subject { service.call }
it 'returns all user areas' do it 'returns all user areas' do
result = service.call expect(subject).to be_an(Array)
expect(result).to be_an(Array) expect(subject.size).to eq(2)
expect(result.size).to eq(2)
end end
it 'excludes user_id and id fields' do it 'excludes user_id and id fields' do
result = service.call subject.each do |area_data|
result.each do |area_data|
expect(area_data).not_to have_key('user_id') expect(area_data).not_to have_key('user_id')
expect(area_data).not_to have_key('id') expect(area_data).not_to have_key('id')
end end
end end
it 'includes expected area attributes' do it 'includes expected area attributes' do
result = service.call area_data = subject.find { |a| a['name'] == 'Home' }
area_data = result.find { |a| a['name'] == 'Home' }
expect(area_data).to include( expect(area_data).to include(
'name' => 'Home', 'name' => 'Home',
@ -51,18 +49,11 @@ RSpec.describe Users::ExportData::Areas, type: :service do
let!(:user_area) { create(:area, user: user, name: 'User Area') } let!(:user_area) { create(:area, user: user, name: 'User Area') }
let!(:other_user_area) { create(:area, user: other_user, name: 'Other User Area') } let!(:other_user_area) { create(:area, user: other_user, name: 'Other User Area') }
it 'only returns areas for the specified user' do subject { service.call }
result = service.call
expect(result.size).to eq(1)
expect(result.first['name']).to eq('User Area')
end
end
end
describe 'private methods' do it 'only returns areas for the specified user' do
describe '#user' do expect(subject.size).to eq(1)
it 'returns the initialized user' do expect(subject.first['name']).to eq('User Area')
expect(service.send(:user)).to eq(user)
end end
end end
end end

View file

@ -7,6 +7,8 @@ RSpec.describe Users::ExportData::Exports, type: :service do
let(:files_directory) { Rails.root.join('tmp', 'test_export_files') } let(:files_directory) { Rails.root.join('tmp', 'test_export_files') }
let(:service) { described_class.new(user, files_directory) } let(:service) { described_class.new(user, files_directory) }
subject { service.call }
before do before do
FileUtils.mkdir_p(files_directory) FileUtils.mkdir_p(files_directory)
allow(Rails.logger).to receive(:info) allow(Rails.logger).to receive(:info)
@ -20,8 +22,7 @@ RSpec.describe Users::ExportData::Exports, type: :service do
describe '#call' do describe '#call' do
context 'when user has no exports' do context 'when user has no exports' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -37,10 +38,10 @@ RSpec.describe Users::ExportData::Exports, type: :service do
end end
it 'returns export data without file information' do it 'returns export data without file information' do
result = service.call expect(subject.size).to eq(1)
expect(result.size).to eq(1)
export_data = subject.first
export_data = result.first
expect(export_data).to include( expect(export_data).to include(
'name' => 'Test Export', 'name' => 'Test Export',
'file_format' => 'json', 'file_format' => 'json',
@ -71,8 +72,7 @@ RSpec.describe Users::ExportData::Exports, type: :service do
end end
it 'returns export data with file information' do it 'returns export data with file information' do
result = service.call export_data = subject.first
export_data = result.first
expect(export_data['name']).to eq('Export with File') expect(export_data['name']).to eq('Export with File')
expect(export_data['file_name']).to eq("export_#{export_with_file.id}_export_data.json") expect(export_data['file_name']).to eq("export_#{export_with_file.id}_export_data.json")
@ -88,9 +88,8 @@ RSpec.describe Users::ExportData::Exports, type: :service do
let!(:other_user_export) { create(:export, user: other_user, name: 'Other User Export') } let!(:other_user_export) { create(:export, user: other_user, name: 'Other User Export') }
it 'only returns exports for the specified user' do it 'only returns exports for the specified user' do
result = service.call expect(subject.size).to eq(1)
expect(result.size).to eq(1) expect(subject.first['name']).to eq('User Export')
expect(result.first['name']).to eq('User Export')
end end
end end
end end

View file

@ -7,6 +7,8 @@ RSpec.describe Users::ExportData::Imports, type: :service do
let(:files_directory) { Pathname.new(Dir.mktmpdir('test_exports')) } let(:files_directory) { Pathname.new(Dir.mktmpdir('test_exports')) }
let(:service) { described_class.new(user, files_directory) } let(:service) { described_class.new(user, files_directory) }
subject { service.call }
after do after do
FileUtils.rm_rf(files_directory) if files_directory.exist? FileUtils.rm_rf(files_directory) if files_directory.exist?
end end
@ -14,8 +16,7 @@ RSpec.describe Users::ExportData::Imports, type: :service do
describe '#call' do describe '#call' do
context 'when user has no imports' do context 'when user has no imports' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -24,10 +25,9 @@ RSpec.describe Users::ExportData::Imports, type: :service do
let!(:import2) { create(:import, user: user, name: 'Import 2') } let!(:import2) { create(:import, user: user, name: 'Import 2') }
it 'returns import data without file information' do it 'returns import data without file information' do
result = service.call expect(service.call.size).to eq(2)
expect(result.size).to eq(2)
first_import = result.find { |i| i['name'] == 'Import 1' } first_import = service.call.find { |i| i['name'] == 'Import 1' }
expect(first_import['file_name']).to be_nil expect(first_import['file_name']).to be_nil
expect(first_import['original_filename']).to be_nil expect(first_import['original_filename']).to be_nil
expect(first_import).not_to have_key('user_id') expect(first_import).not_to have_key('user_id')
@ -57,8 +57,7 @@ RSpec.describe Users::ExportData::Imports, type: :service do
end end
it 'returns import data with file information' do it 'returns import data with file information' do
result = service.call import_data = subject.first
import_data = result.first
expect(import_data['name']).to eq('Import with File') expect(import_data['name']).to eq('Import with File')
expect(import_data['file_name']).to eq("import_#{import_with_file.id}_test_file.json") expect(import_data['file_name']).to eq("import_#{import_with_file.id}_test_file.json")
@ -68,8 +67,7 @@ RSpec.describe Users::ExportData::Imports, type: :service do
end end
it 'downloads and saves the file to the files directory' do it 'downloads and saves the file to the files directory' do
result = service.call import_data = subject.first
import_data = result.first
file_path = files_directory.join(import_data['file_name']) file_path = files_directory.join(import_data['file_name'])
expect(File.exist?(file_path)).to be true expect(File.exist?(file_path)).to be true
@ -80,8 +78,7 @@ RSpec.describe Users::ExportData::Imports, type: :service do
blob = create_blob(filename: 'test file with spaces & symbols!.json') blob = create_blob(filename: 'test file with spaces & symbols!.json')
import_with_file.file.attach(blob) import_with_file.file.attach(blob)
result = service.call import_data = subject.first
import_data = result.first
expect(import_data['file_name']).to match(/import_\d+_test_file_with_spaces___symbols_.json/) expect(import_data['file_name']).to match(/import_\d+_test_file_with_spaces___symbols_.json/)
end end
@ -99,8 +96,7 @@ RSpec.describe Users::ExportData::Imports, type: :service do
end end
it 'handles download errors gracefully' do it 'handles download errors gracefully' do
result = service.call import_data = subject.find { |i| i['name'] == 'Import with error file' }
import_data = result.find { |i| i['name'] == 'Import with error file' }
expect(import_data['file_error']).to eq('Failed to download: Download failed') expect(import_data['file_error']).to eq('Failed to download: Download failed')
end end
@ -128,8 +124,7 @@ RSpec.describe Users::ExportData::Imports, type: :service do
end end
it 'returns all imports' do it 'returns all imports' do
result = service.call expect(subject.size).to eq(3)
expect(result.size).to eq(3)
end end
end end
@ -139,9 +134,8 @@ RSpec.describe Users::ExportData::Imports, type: :service do
let!(:other_user_import) { create(:import, user: other_user, name: 'Other User Import') } let!(:other_user_import) { create(:import, user: other_user, name: 'Other User Import') }
it 'only returns imports for the specified user' do it 'only returns imports for the specified user' do
result = service.call expect(subject.size).to eq(1)
expect(result.size).to eq(1) expect(subject.first['name']).to eq('User Import')
expect(result.first['name']).to eq('User Import')
end end
end end
@ -165,79 +159,6 @@ RSpec.describe Users::ExportData::Imports, type: :service do
end end
end end
describe 'private methods' do
let(:import) { create(:import, user: user, name: 'Test Import') }
describe '#process_import' do
context 'with import without file' do
it 'processes import correctly' do
result = service.send(:process_import, import)
expect(result).to include(
'name' => 'Test Import',
'file_name' => nil,
'original_filename' => nil
)
expect(result).not_to have_key('user_id')
expect(result).not_to have_key('raw_data')
expect(result).not_to have_key('id')
end
end
context 'with import with file' do
let(:blob) { create_blob(filename: 'test.json', content_type: 'application/json') }
before do
import.file.attach(blob)
allow(Imports::SecureFileDownloader).to receive(:new).and_return(
double(download_with_verification: 'file content')
)
end
it 'processes import with file data' do
result = service.send(:process_import, import)
expect(result['file_name']).to be_present
expect(result['original_filename']).to eq('test.json')
expect(result['content_type']).to eq('application/json')
end
end
end
describe '#generate_sanitized_filename' do
let(:import) { create(:import, user: user, name: 'Filename test import') }
let(:blob) { create_blob(filename: 'test/file<>:"|?*\\.json') }
before { import.file.attach(blob) }
it 'sanitizes filename correctly' do
result = service.send(:generate_sanitized_filename, import)
expect(result).to eq("import_#{import.id}_test-file--------.json")
end
end
describe '#add_file_metadata_to_import' do
let(:import) { create(:import, user: user) }
let(:import_hash) { {} }
let(:filename) { 'sanitized_filename.json' }
let(:blob) { create_blob(filename: 'original.json', content_type: 'application/json') }
before { import.file.attach(blob) }
it 'adds correct metadata to import hash' do
service.send(:add_file_metadata_to_import, import, import_hash, filename)
expect(import_hash).to include(
'file_name' => 'sanitized_filename.json',
'original_filename' => 'original.json',
'file_size' => blob.byte_size,
'content_type' => 'application/json'
)
end
end
end
private private
def create_blob(filename: 'test.txt', content_type: 'text/plain') def create_blob(filename: 'test.txt', content_type: 'text/plain')

View file

@ -6,11 +6,12 @@ RSpec.describe Users::ExportData::Notifications, type: :service do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { described_class.new(user) } let(:service) { described_class.new(user) }
subject { service.call }
describe '#call' do describe '#call' do
context 'when user has no notifications' do context 'when user has no notifications' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -19,23 +20,19 @@ RSpec.describe Users::ExportData::Notifications, type: :service do
let!(:notification2) { create(:notification, user: user, title: 'Test 2', kind: :warning) } let!(:notification2) { create(:notification, user: user, title: 'Test 2', kind: :warning) }
it 'returns all user notifications' do it 'returns all user notifications' do
result = service.call expect(subject).to be_an(Array)
expect(result).to be_an(Array) expect(subject.size).to eq(2)
expect(result.size).to eq(2)
end end
it 'excludes user_id and id fields' do it 'excludes user_id and id fields' do
result = service.call subject.each do |notification_data|
result.each do |notification_data|
expect(notification_data).not_to have_key('user_id') expect(notification_data).not_to have_key('user_id')
expect(notification_data).not_to have_key('id') expect(notification_data).not_to have_key('id')
end end
end end
it 'includes expected notification attributes' do it 'includes expected notification attributes' do
result = service.call notification_data = subject.find { |n| n['title'] == 'Test 1' }
notification_data = result.find { |n| n['title'] == 'Test 1' }
expect(notification_data).to include( expect(notification_data).to include(
'title' => 'Test 1', 'title' => 'Test 1',
@ -52,9 +49,8 @@ RSpec.describe Users::ExportData::Notifications, type: :service do
let!(:other_user_notification) { create(:notification, user: other_user, title: 'Other Notification') } let!(:other_user_notification) { create(:notification, user: other_user, title: 'Other Notification') }
it 'only returns notifications for the specified user' do it 'only returns notifications for the specified user' do
result = service.call expect(subject.size).to eq(1)
expect(result.size).to eq(1) expect(subject.first['title']).to eq('User Notification')
expect(result.first['title']).to eq('User Notification')
end end
end end
end end

View file

@ -6,11 +6,12 @@ RSpec.describe Users::ExportData::Places, type: :service do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { described_class.new(user) } let(:service) { described_class.new(user) }
subject { service.call }
describe '#call' do describe '#call' do
context 'when user has no places' do context 'when user has no places' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -21,21 +22,17 @@ RSpec.describe Users::ExportData::Places, type: :service do
let!(:visit2) { create(:visit, user: user, place: place2) } let!(:visit2) { create(:visit, user: user, place: place2) }
it 'returns all places' do it 'returns all places' do
result = service.call expect(subject.size).to eq(2)
expect(result.size).to eq(2)
end end
it 'excludes id field' do it 'excludes id field' do
result = service.call subject.each do |place_data|
result.each do |place_data|
expect(place_data).not_to have_key('id') expect(place_data).not_to have_key('id')
end end
end end
it 'includes expected place attributes' do it 'includes expected place attributes' do
result = service.call place_data = subject.find { |p| p['name'] == 'Office' }
place_data = result.find { |p| p['name'] == 'Office' }
expect(place_data).to include( expect(place_data).to include(
'name' => 'Office', 'name' => 'Office',

View file

@ -6,11 +6,12 @@ RSpec.describe Users::ExportData::Points, type: :service do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { described_class.new(user) } let(:service) { described_class.new(user) }
subject { service.call }
describe '#call' do describe '#call' do
context 'when user has no points' do context 'when user has no points' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -66,14 +67,12 @@ RSpec.describe Users::ExportData::Points, type: :service do
end end
it 'returns all points with correct structure' do it 'returns all points with correct structure' do
result = service.call expect(subject).to be_an(Array)
expect(result).to be_an(Array) expect(subject.size).to eq(2)
expect(result.size).to eq(2)
end end
it 'includes all point attributes for point with relationships' do it 'includes all point attributes for point with relationships' do
result = service.call point_data = subject.find { |p| p['external_track_id'] == 'ext-123' }
point_data = result.find { |p| p['external_track_id'] == 'ext-123' }
expect(point_data).to include( expect(point_data).to include(
'battery_status' => 2, # enum value for :charging 'battery_status' => 2, # enum value for :charging
@ -109,8 +108,7 @@ RSpec.describe Users::ExportData::Points, type: :service do
end end
it 'includes import reference when point has import' do it 'includes import reference when point has import' do
result = service.call point_data = subject.find { |p| p['external_track_id'] == 'ext-123' }
point_data = result.find { |p| p['external_track_id'] == 'ext-123' }
expect(point_data['import_reference']).to eq({ expect(point_data['import_reference']).to eq({
'name' => 'Test Import', 'name' => 'Test Import',
@ -120,8 +118,7 @@ RSpec.describe Users::ExportData::Points, type: :service do
end end
it 'includes country info when point has country' do it 'includes country info when point has country' do
result = service.call point_data = subject.find { |p| p['external_track_id'] == 'ext-123' }
point_data = result.find { |p| p['external_track_id'] == 'ext-123' }
# Since we're using LEFT JOIN and the country is properly associated, # Since we're using LEFT JOIN and the country is properly associated,
# this should work, but let's check if it's actually being set # this should work, but let's check if it's actually being set
@ -138,8 +135,7 @@ RSpec.describe Users::ExportData::Points, type: :service do
end end
it 'includes visit reference when point has visit' do it 'includes visit reference when point has visit' do
result = service.call point_data = subject.find { |p| p['external_track_id'] == 'ext-123' }
point_data = result.find { |p| p['external_track_id'] == 'ext-123' }
expect(point_data['visit_reference']).to eq({ expect(point_data['visit_reference']).to eq({
'name' => 'Work Visit', 'name' => 'Work Visit',
@ -149,8 +145,7 @@ RSpec.describe Users::ExportData::Points, type: :service do
end end
it 'does not include relationships for points without them' do it 'does not include relationships for points without them' do
result = service.call point_data = subject.find { |p| p['external_track_id'].nil? }
point_data = result.find { |p| p['external_track_id'].nil? }
expect(point_data['import_reference']).to be_nil expect(point_data['import_reference']).to be_nil
expect(point_data['country_info']).to be_nil expect(point_data['country_info']).to be_nil
@ -158,21 +153,19 @@ RSpec.describe Users::ExportData::Points, type: :service do
end end
it 'correctly extracts longitude and latitude from lonlat geometry' do it 'correctly extracts longitude and latitude from lonlat geometry' do
result = service.call point1 = subject.find { |p| p['external_track_id'] == 'ext-123' }
point1 = result.find { |p| p['external_track_id'] == 'ext-123' }
expect(point1['longitude']).to eq(-74.006) expect(point1['longitude']).to eq(-74.006)
expect(point1['latitude']).to eq(40.7128) expect(point1['latitude']).to eq(40.7128)
point2 = result.find { |p| p['external_track_id'].nil? } point2 = subject.find { |p| p['external_track_id'].nil? }
expect(point2['longitude']).to eq(-73.9857) expect(point2['longitude']).to eq(-73.9857)
expect(point2['latitude']).to eq(40.7484) expect(point2['latitude']).to eq(40.7484)
end end
it 'orders points by id' do it 'orders points by id' do
result = service.call expect(subject.first['timestamp']).to eq(1640995200)
expect(result.first['timestamp']).to eq(1640995200) expect(subject.last['timestamp']).to eq(1640995260)
expect(result.last['timestamp']).to eq(1640995260)
end end
it 'logs processing information' do it 'logs processing information' do
@ -187,8 +180,7 @@ RSpec.describe Users::ExportData::Points, type: :service do
end end
it 'handles null values gracefully' do it 'handles null values gracefully' do
result = service.call point_data = subject.first
point_data = result.first
expect(point_data['inrids']).to eq([]) expect(point_data['inrids']).to eq([])
expect(point_data['in_regions']).to eq([]) expect(point_data['in_regions']).to eq([])
@ -200,9 +192,10 @@ RSpec.describe Users::ExportData::Points, type: :service do
let!(:user_point) { create(:point, user: user) } let!(:user_point) { create(:point, user: user) }
let!(:other_user_point) { create(:point, user: other_user) } let!(:other_user_point) { create(:point, user: other_user) }
subject { service.call }
it 'only returns points for the specified user' do it 'only returns points for the specified user' do
result = service.call expect(service.call.size).to eq(1)
expect(result.size).to eq(1)
end end
end end
@ -211,19 +204,11 @@ RSpec.describe Users::ExportData::Points, type: :service do
it 'uses a single optimized query' do it 'uses a single optimized query' do
expect(Rails.logger).to receive(:info).with('Processing 3 points for export...') expect(Rails.logger).to receive(:info).with('Processing 3 points for export...')
service.call subject
end end
it 'avoids N+1 queries by using joins' do it 'avoids N+1 queries by using joins' do
expect(service.call.size).to eq(3) expect(subject.size).to eq(3)
end
end
end
describe 'private methods' do
describe '#user' do
it 'returns the initialized user' do
expect(service.send(:user)).to eq(user)
end end
end end
end end

View file

@ -6,11 +6,12 @@ RSpec.describe Users::ExportData::Stats, type: :service do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { described_class.new(user) } let(:service) { described_class.new(user) }
subject { service.call }
describe '#call' do describe '#call' do
context 'when user has no stats' do context 'when user has no stats' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -19,23 +20,19 @@ RSpec.describe Users::ExportData::Stats, type: :service do
let!(:stat2) { create(:stat, user: user, year: 2024, month: 2, distance: 150) } let!(:stat2) { create(:stat, user: user, year: 2024, month: 2, distance: 150) }
it 'returns all user stats' do it 'returns all user stats' do
result = service.call expect(subject).to be_an(Array)
expect(result).to be_an(Array) expect(subject.size).to eq(2)
expect(result.size).to eq(2)
end end
it 'excludes user_id and id fields' do it 'excludes user_id and id fields' do
result = service.call subject.each do |stat_data|
result.each do |stat_data|
expect(stat_data).not_to have_key('user_id') expect(stat_data).not_to have_key('user_id')
expect(stat_data).not_to have_key('id') expect(stat_data).not_to have_key('id')
end end
end end
it 'includes expected stat attributes' do it 'includes expected stat attributes' do
result = service.call stat_data = subject.find { |s| s['month'] == 1 }
stat_data = result.find { |s| s['month'] == 1 }
expect(stat_data).to include( expect(stat_data).to include(
'year' => 2024, 'year' => 2024,
@ -53,8 +50,7 @@ RSpec.describe Users::ExportData::Stats, type: :service do
let!(:other_user_stat) { create(:stat, user: other_user, year: 2024, month: 1) } let!(:other_user_stat) { create(:stat, user: other_user, year: 2024, month: 1) }
it 'only returns stats for the specified user' do it 'only returns stats for the specified user' do
result = service.call expect(subject.size).to eq(1)
expect(result.size).to eq(1)
end end
end end
end end

View file

@ -6,11 +6,12 @@ RSpec.describe Users::ExportData::Trips, type: :service do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { described_class.new(user) } let(:service) { described_class.new(user) }
subject { service.call }
describe '#call' do describe '#call' do
context 'when user has no trips' do context 'when user has no trips' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -19,23 +20,19 @@ RSpec.describe Users::ExportData::Trips, type: :service do
let!(:trip2) { create(:trip, user: user, name: 'Vacation', distance: 1200) } let!(:trip2) { create(:trip, user: user, name: 'Vacation', distance: 1200) }
it 'returns all user trips' do it 'returns all user trips' do
result = service.call expect(subject).to be_an(Array)
expect(result).to be_an(Array) expect(subject.size).to eq(2)
expect(result.size).to eq(2)
end end
it 'excludes user_id and id fields' do it 'excludes user_id and id fields' do
result = service.call subject.each do |trip_data|
result.each do |trip_data|
expect(trip_data).not_to have_key('user_id') expect(trip_data).not_to have_key('user_id')
expect(trip_data).not_to have_key('id') expect(trip_data).not_to have_key('id')
end end
end end
it 'includes expected trip attributes' do it 'includes expected trip attributes' do
result = service.call trip_data = subject.find { |t| t['name'] == 'Business Trip' }
trip_data = result.find { |t| t['name'] == 'Business Trip' }
expect(trip_data).to include( expect(trip_data).to include(
'name' => 'Business Trip', 'name' => 'Business Trip',
@ -51,10 +48,11 @@ RSpec.describe Users::ExportData::Trips, type: :service do
let!(:user_trip) { create(:trip, user: user, name: 'User Trip') } let!(:user_trip) { create(:trip, user: user, name: 'User Trip') }
let!(:other_user_trip) { create(:trip, user: other_user, name: 'Other Trip') } let!(:other_user_trip) { create(:trip, user: other_user, name: 'Other Trip') }
subject { service.call }
it 'only returns trips for the specified user' do it 'only returns trips for the specified user' do
result = service.call expect(service.call.size).to eq(1)
expect(result.size).to eq(1) expect(service.call.first['name']).to eq('User Trip')
expect(result.first['name']).to eq('User Trip')
end end
end end
end end

View file

@ -6,11 +6,12 @@ RSpec.describe Users::ExportData::Visits, type: :service do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { described_class.new(user) } let(:service) { described_class.new(user) }
subject { service.call }
describe '#call' do describe '#call' do
context 'when user has no visits' do context 'when user has no visits' do
it 'returns an empty array' do it 'returns an empty array' do
result = service.call expect(subject).to eq([])
expect(result).to eq([])
end end
end end
@ -29,14 +30,12 @@ RSpec.describe Users::ExportData::Visits, type: :service do
end end
it 'returns visits with place references' do it 'returns visits with place references' do
result = service.call expect(subject).to be_an(Array)
expect(result).to be_an(Array) expect(subject.size).to eq(1)
expect(result.size).to eq(1)
end end
it 'excludes user_id, place_id, and id fields' do it 'excludes user_id, place_id, and id fields' do
result = service.call visit_data = subject.first
visit_data = result.first
expect(visit_data).not_to have_key('user_id') expect(visit_data).not_to have_key('user_id')
expect(visit_data).not_to have_key('place_id') expect(visit_data).not_to have_key('place_id')
@ -44,8 +43,7 @@ RSpec.describe Users::ExportData::Visits, type: :service do
end end
it 'includes visit attributes and place reference' do it 'includes visit attributes and place reference' do
result = service.call visit_data = subject.first
visit_data = result.first
expect(visit_data).to include( expect(visit_data).to include(
'name' => 'Work Visit', 'name' => 'Work Visit',
@ -64,8 +62,7 @@ RSpec.describe Users::ExportData::Visits, type: :service do
end end
it 'includes created_at and updated_at timestamps' do it 'includes created_at and updated_at timestamps' do
result = service.call visit_data = subject.first
visit_data = result.first
expect(visit_data).to have_key('created_at') expect(visit_data).to have_key('created_at')
expect(visit_data).to have_key('updated_at') expect(visit_data).to have_key('updated_at')
@ -86,8 +83,7 @@ RSpec.describe Users::ExportData::Visits, type: :service do
end end
it 'returns visits with null place references' do it 'returns visits with null place references' do
result = service.call visit_data = subject.first
visit_data = result.first
expect(visit_data).to include( expect(visit_data).to include(
'name' => 'Unknown Location', 'name' => 'Unknown Location',
@ -104,11 +100,10 @@ RSpec.describe Users::ExportData::Visits, type: :service do
let!(:visit_without_place) { create(:visit, user: user, place: nil, name: 'Random Stop') } let!(:visit_without_place) { create(:visit, user: user, place: nil, name: 'Random Stop') }
it 'returns all visits with appropriate place references' do it 'returns all visits with appropriate place references' do
result = service.call expect(subject.size).to eq(2)
expect(result.size).to eq(2)
visit_with_place_data = result.find { |v| v['name'] == 'Workout' } visit_with_place_data = subject.find { |v| v['name'] == 'Workout' }
visit_without_place_data = result.find { |v| v['name'] == 'Random Stop' } visit_without_place_data = subject.find { |v| v['name'] == 'Random Stop' }
expect(visit_with_place_data['place_reference']).to be_present expect(visit_with_place_data['place_reference']).to be_present
expect(visit_without_place_data['place_reference']).to be_nil expect(visit_without_place_data['place_reference']).to be_nil
@ -121,9 +116,8 @@ RSpec.describe Users::ExportData::Visits, type: :service do
let!(:other_user_visit) { create(:visit, user: other_user, name: 'Other User Visit') } let!(:other_user_visit) { create(:visit, user: other_user, name: 'Other User Visit') }
it 'only returns visits for the specified user' do it 'only returns visits for the specified user' do
result = service.call expect(subject.size).to eq(1)
expect(result.size).to eq(1) expect(subject.first['name']).to eq('User Visit')
expect(result.first['name']).to eq('User Visit')
end end
end end
@ -135,15 +129,8 @@ RSpec.describe Users::ExportData::Visits, type: :service do
# This test verifies that we're using .includes(:place) # This test verifies that we're using .includes(:place)
expect(user.visits).to receive(:includes).with(:place).and_call_original expect(user.visits).to receive(:includes).with(:place).and_call_original
service.call
end
end
end
describe 'private methods' do subject
describe '#user' do
it 'returns the initialized user' do
expect(service.send(:user)).to eq(user)
end end
end end
end end