# frozen_string_literal: true require 'rails_helper' require 'tmpdir' require 'oj' 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)) allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:rm_rf) allow_any_instance_of(Pathname).to receive(:exist?).and_return(true) end describe '#import' do let(:notification_double) { instance_double(::Notifications::Create, call: true) } before do allow(::Notifications::Create).to receive(:new).and_return(notification_double) allow(service).to receive(:cleanup_temporary_files) end context 'when import succeeds' do before do allow(service).to receive(:extract_archive) allow(service).to receive(:process_archive_data) do stats = service.instance_variable_get(:@import_stats) stats[:settings_updated] = true stats[:areas_created] = 2 stats[:places_created] = 3 stats[:imports_created] = 1 stats[:exports_created] = 1 stats[:trips_created] = 2 stats[:stats_created] = 1 stats[:notifications_created] = 2 stats[:visits_created] = 4 stats[:points_created] = 1000 stats[:files_restored] = 7 end end it 'creates the import directory' do expect(FileUtils).to receive(:mkdir_p).with(import_directory) service.import end it 'extracts the archive and processes data' do expect(service).to receive(:extract_archive).ordered expect(service).to receive(:process_archive_data).ordered service.import end it 'creates a success notification with summary' do expect(::Notifications::Create).to receive(:new).with( user: user, title: 'Data import completed', content: include('1000 points, 4 visits, 3 places, 2 trips'), kind: :info ) 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 end context 'when an error happens during processing' do let(:error_message) { 'boom' } before do allow(service).to receive(:extract_archive) allow(service).to receive(:process_archive_data).and_raise(StandardError, error_message) allow(ExceptionReporter).to receive(:call) end it 'creates a failure notification and re-raises the error' 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 end end describe '#process_archive_data' do let(:tmp_dir) { Pathname.new(Dir.mktmpdir) } let(:json_path) { tmp_dir.join('data.json') } let(:places_calls) { [] } let(:visits_batches) { [] } let(:points_ingested) { [] } let(:points_importer) do instance_double(Users::ImportData::Points, add: nil, finalize: 2) end before do payload = { 'counts' => { 'places' => 2, 'visits' => 2, 'points' => 2 }, 'settings' => { 'theme' => 'dark' }, 'areas' => [], 'imports' => [], 'exports' => [], 'trips' => [], 'stats' => [], 'notifications' => [], 'places' => [ { 'name' => 'Cafe', 'latitude' => 1.0, 'longitude' => 2.0 }, { 'name' => 'Library', 'latitude' => 3.0, 'longitude' => 4.0 } ], 'visits' => [ { 'name' => 'Morning Coffee', 'started_at' => '2025-01-01T09:00:00Z', 'ended_at' => '2025-01-01T10:00:00Z' }, { 'name' => 'Study Time', 'started_at' => '2025-01-01T12:00:00Z', 'ended_at' => '2025-01-01T14:00:00Z' } ], 'points' => [ { 'timestamp' => 1, 'lonlat' => 'POINT(2 1)' }, { 'timestamp' => 2, 'lonlat' => 'POINT(4 3)' } ] } File.write(json_path, Oj.dump(payload, mode: :compat)) service.instance_variable_set(:@import_directory, tmp_dir) allow(Users::ImportData::Settings).to receive(:new).and_return(double(call: true)) allow(Users::ImportData::Areas).to receive(:new).and_return(double(call: 0)) allow(Users::ImportData::Imports).to receive(:new).and_return(double(call: [0, 0])) allow(Users::ImportData::Exports).to receive(:new).and_return(double(call: [0, 0])) allow(Users::ImportData::Trips).to receive(:new).and_return(double(call: 0)) allow(Users::ImportData::Stats).to receive(:new).and_return(double(call: 0)) allow(Users::ImportData::Notifications).to receive(:new).and_return(double(call: 0)) allow(Users::ImportData::Places).to receive(:new) do |_, batch| places_calls << batch double(call: batch.size) end allow(Users::ImportData::Visits).to receive(:new) do |_, batch| visits_batches << batch double(call: batch.size) end allow(points_importer).to receive(:add) do |point| points_ingested << point end allow(Users::ImportData::Points).to receive(:new) do |_, points_data, batch_size:| expect(points_data).to be_nil expect(batch_size).to eq(described_class::STREAM_BATCH_SIZE) points_importer end end after do FileUtils.remove_entry(tmp_dir) end it 'streams sections and updates import stats' do service.send(:process_archive_data) expect(places_calls.flatten.map { |place| place['name'] }).to contain_exactly('Cafe', 'Library') expect(visits_batches.flatten.map { |visit| visit['name'] }).to contain_exactly('Morning Coffee', 'Study Time') expect(points_ingested.map { |point| point['timestamp'] }).to eq([1, 2]) stats = service.instance_variable_get(:@import_stats) expect(stats[:places_created]).to eq(2) expect(stats[:visits_created]).to eq(2) expect(stats[:points_created]).to eq(2) end end end