dawarich/spec/services/users/import_data_spec.rb

298 lines
11 KiB
Ruby
Raw Normal View History

2025-06-28 06:22:56 -04:00
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Users::ImportData, type: :service do
let(:user) { create(:user) }
let(:archive_path) { Rails.root.join('tmp', 'test_export.zip') }
let(:service) { described_class.new(user, archive_path) }
let(:import_directory) { Rails.root.join('tmp', "import_#{user.email.gsub(/[^0-9A-Za-z._-]/, '_')}_1234567890") }
before do
allow(Time).to receive(:current).and_return(Time.zone.at(1234567890))
2025-06-28 06:22:56 -04:00
allow(FileUtils).to receive(:mkdir_p)
allow(FileUtils).to receive(:rm_rf)
allow(File).to receive(:directory?).and_return(true)
end
describe '#import' do
let(:sample_data) do
{
'counts' => {
'areas' => 2,
'places' => 3,
'imports' => 1,
'exports' => 1,
'trips' => 2,
'stats' => 1,
'notifications' => 2,
'visits' => 4,
'points' => 1000
},
'settings' => { 'theme' => 'dark' },
'areas' => [{ 'name' => 'Home', 'latitude' => '40.7128', 'longitude' => '-74.0060' }],
'places' => [{ 'name' => 'Office', 'latitude' => '40.7589', 'longitude' => '-73.9851' }],
'imports' => [{ 'name' => 'test.json', 'source' => 'owntracks' }],
'exports' => [{ 'name' => 'export.json', 'status' => 'completed' }],
'trips' => [{ 'name' => 'Trip to NYC', 'distance' => 100.5 }],
'stats' => [{ 'year' => 2024, 'month' => 1, 'distance' => 456.78 }],
'notifications' => [{ 'title' => 'Test', 'content' => 'Test notification' }],
'visits' => [{ 'name' => 'Work Visit', 'duration' => 3600 }],
'points' => [{ 'latitude' => 40.7128, 'longitude' => -74.0060, 'timestamp' => 1234567890 }]
}
end
before do
# Mock ZIP file extraction
zipfile_mock = double('ZipFile')
allow(zipfile_mock).to receive(:each)
allow(Zip::File).to receive(:open).with(archive_path).and_yield(zipfile_mock)
# Mock JSON loading and File operations
allow(File).to receive(:exist?).and_return(false)
allow(File).to receive(:exist?).with(import_directory.join('data.json')).and_return(true)
allow(File).to receive(:read).with(import_directory.join('data.json')).and_return(sample_data.to_json)
# Mock all import services
allow(Users::ImportData::Settings).to receive(:new).and_return(double(call: true))
allow(Users::ImportData::Areas).to receive(:new).and_return(double(call: 2))
allow(Users::ImportData::Places).to receive(:new).and_return(double(call: 3))
allow(Users::ImportData::Imports).to receive(:new).and_return(double(call: [1, 5]))
allow(Users::ImportData::Exports).to receive(:new).and_return(double(call: [1, 2]))
allow(Users::ImportData::Trips).to receive(:new).and_return(double(call: 2))
allow(Users::ImportData::Stats).to receive(:new).and_return(double(call: 1))
allow(Users::ImportData::Notifications).to receive(:new).and_return(double(call: 2))
allow(Users::ImportData::Visits).to receive(:new).and_return(double(call: 4))
allow(Users::ImportData::Points).to receive(:new).and_return(double(call: 1000))
# Mock notifications
allow(::Notifications::Create).to receive(:new).and_return(double(call: true))
# Mock cleanup
allow(service).to receive(:cleanup_temporary_files)
allow_any_instance_of(Pathname).to receive(:exist?).and_return(true)
end
context 'when import is successful' do
it 'creates import directory' do
expect(FileUtils).to receive(:mkdir_p).with(import_directory)
service.import
end
it 'extracts the archive' do
expect(Zip::File).to receive(:open).with(archive_path)
service.import
end
it 'loads JSON data from extracted files' do
expect(File).to receive(:exist?).with(import_directory.join('data.json'))
expect(File).to receive(:read).with(import_directory.join('data.json'))
service.import
end
it 'calls all import services in correct order' do
expect(Users::ImportData::Settings).to receive(:new).with(user, sample_data['settings']).ordered
expect(Users::ImportData::Areas).to receive(:new).with(user, sample_data['areas']).ordered
expect(Users::ImportData::Places).to receive(:new).with(user, sample_data['places']).ordered
expect(Users::ImportData::Imports).to receive(:new).with(user, sample_data['imports'], import_directory.join('files')).ordered
expect(Users::ImportData::Exports).to receive(:new).with(user, sample_data['exports'], import_directory.join('files')).ordered
expect(Users::ImportData::Trips).to receive(:new).with(user, sample_data['trips']).ordered
expect(Users::ImportData::Stats).to receive(:new).with(user, sample_data['stats']).ordered
expect(Users::ImportData::Notifications).to receive(:new).with(user, sample_data['notifications']).ordered
expect(Users::ImportData::Visits).to receive(:new).with(user, sample_data['visits']).ordered
expect(Users::ImportData::Points).to receive(:new).with(user, sample_data['points']).ordered
service.import
end
it 'creates success notification with import stats' do
expect(::Notifications::Create).to receive(:new).with(
user: user,
title: 'Data import completed',
content: match(/1000 points.*4 visits.*3 places.*2 trips/),
kind: :info
)
service.import
end
it 'cleans up temporary files' do
expect(service).to receive(:cleanup_temporary_files).with(import_directory)
service.import
end
it 'returns import statistics' do
result = service.import
expect(result).to include(
settings_updated: true,
areas_created: 2,
places_created: 3,
imports_created: 1,
exports_created: 1,
trips_created: 2,
stats_created: 1,
notifications_created: 2,
visits_created: 4,
points_created: 1000,
files_restored: 7
)
end
it 'logs expected counts if available' do
allow(Rails.logger).to receive(:info) # Allow other log messages
expect(Rails.logger).to receive(:info).with(/Expected entity counts from export:/)
service.import
end
end
context 'when JSON file is missing' do
before do
allow(File).to receive(:exist?).and_return(false)
allow(File).to receive(:exist?).with(import_directory.join('data.json')).and_return(false)
allow(ExceptionReporter).to receive(:call)
end
it 'raises an error' do
expect { service.import }.to raise_error(StandardError, 'Data file not found in archive: data.json')
end
end
context 'when JSON is invalid' do
before do
allow(File).to receive(:exist?).and_return(false)
allow(File).to receive(:exist?).with(import_directory.join('data.json')).and_return(true)
allow(File).to receive(:read).with(import_directory.join('data.json')).and_return('invalid json')
allow(ExceptionReporter).to receive(:call)
end
it 'raises a JSON parse error' do
expect { service.import }.to raise_error(StandardError, /Invalid JSON format in data file/)
end
end
context 'when an error occurs during import' do
let(:error_message) { 'Something went wrong' }
before do
allow(File).to receive(:exist?).and_return(false)
allow(File).to receive(:exist?).with(import_directory.join('data.json')).and_return(true)
allow(File).to receive(:read).with(import_directory.join('data.json')).and_return(sample_data.to_json)
allow(Users::ImportData::Settings).to receive(:new).and_raise(StandardError, error_message)
allow(ExceptionReporter).to receive(:call)
allow(::Notifications::Create).to receive(:new).and_return(double(call: true))
end
it 'creates failure notification' do
expect(::Notifications::Create).to receive(:new).with(
user: user,
title: 'Data import failed',
content: "Your data import failed with error: #{error_message}. Please check the archive format and try again.",
kind: :error
)
expect { service.import }.to raise_error(StandardError, error_message)
end
it 'reports error via ExceptionReporter' do
expect(ExceptionReporter).to receive(:call).with(
an_instance_of(StandardError),
'Data import failed'
)
expect { service.import }.to raise_error(StandardError, error_message)
end
it 'still cleans up temporary files' do
expect(service).to receive(:cleanup_temporary_files)
expect { service.import }.to raise_error(StandardError, error_message)
end
it 're-raises the error' do
expect { service.import }.to raise_error(StandardError, error_message)
end
end
context 'when data sections are missing' do
let(:minimal_data) { { 'settings' => { 'theme' => 'dark' } } }
before do
# Reset JSON file mocking
allow(File).to receive(:exist?).and_return(false)
allow(File).to receive(:exist?).with(import_directory.join('data.json')).and_return(true)
allow(File).to receive(:read).with(import_directory.join('data.json')).and_return(minimal_data.to_json)
# Only expect Settings to be called
allow(Users::ImportData::Settings).to receive(:new).and_return(double(call: true))
allow(::Notifications::Create).to receive(:new).and_return(double(call: true))
end
it 'only imports available sections' do
expect(Users::ImportData::Settings).to receive(:new).with(user, minimal_data['settings'])
expect(Users::ImportData::Areas).not_to receive(:new)
expect(Users::ImportData::Places).not_to receive(:new)
service.import
end
end
end
describe 'private methods' do
describe '#cleanup_temporary_files' do
context 'when directory exists' do
before do
allow(File).to receive(:directory?).and_return(true)
allow(Rails.logger).to receive(:info)
end
it 'removes the directory' do
expect(FileUtils).to receive(:rm_rf).with(import_directory)
service.send(:cleanup_temporary_files, import_directory)
end
it 'logs the cleanup' do
expect(Rails.logger).to receive(:info).with("Cleaning up temporary import directory: #{import_directory}")
service.send(:cleanup_temporary_files, import_directory)
end
end
context 'when cleanup fails' do
before do
allow(File).to receive(:directory?).and_return(true)
allow(FileUtils).to receive(:rm_rf).and_raise(StandardError, 'Permission denied')
allow(ExceptionReporter).to receive(:call)
end
it 'reports error via ExceptionReporter but does not re-raise' do
expect(ExceptionReporter).to receive(:call).with(
an_instance_of(StandardError),
'Failed to cleanup temporary files'
)
expect { service.send(:cleanup_temporary_files, import_directory) }.not_to raise_error
end
end
context 'when directory does not exist' do
before do
allow(File).to receive(:directory?).and_return(false)
end
it 'does not attempt cleanup' do
expect(FileUtils).not_to receive(:rm_rf)
service.send(:cleanup_temporary_files, import_directory)
end
end
end
end
end