cartodb-4.42/spec/models/geocoding_spec.rb
2024-04-06 05:25:13 +00:00

365 lines
16 KiB
Ruby

require 'spec_helper'
require 'mock_redis'
describe Geocoding do
before(:all) do
@user = create_user(geocoding_quota: 200, geocoding_block_price: 1500, geocoder_provider: 'heremaps')
bypass_named_maps
@table = FactoryGirl.create(:user_table, user_id: @user.id)
end
before(:each) do
bypass_named_maps
end
after(:all) do
bypass_named_maps
@user.destroy
end
describe '#setup' do
let(:geocoding) { FactoryGirl.create(:geocoding, user: @user, user_table: @table, formatter: 'foo', kind: 'admin0') }
it 'sets default timestamps value' do
geocoding.created_at.should_not be_nil
geocoding.updated_at.should_not be_nil
end
it 'links user and geocoding' do
geocoding.user.should eq @user
end
end
describe '#table_geocoder' do
it 'returns an instance of TableGeocoder when kind is high-resolution' do
geocoding = FactoryGirl.build(:geocoding, user: @user, user_table: @table, kind: 'high-resolution', formatter: 'foo')
geocoding.table_geocoder.should be_kind_of(CartoDB::TableGeocoder)
end
it 'returns an instance of InternalGeocoder when kind is not high-resolution' do
geocoding = FactoryGirl.build(:geocoding, user: @user, user_table: @table, kind: 'admin0', geometry_type: 'polygon', formatter: 'foo')
geocoding.table_geocoder.should be_kind_of(CartoDB::InternalGeocoder::Geocoder)
end
it 'memoizes' do
geocoding = FactoryGirl.build(:geocoding, user: @user, user_table: @table, kind: 'admin0', geometry_type: 'polygon', formatter: 'foo')
geocoder = geocoding.table_geocoder
geocoder.should be_kind_of(CartoDB::InternalGeocoder::Geocoder)
geocoder.should eq geocoding.table_geocoder
end
end
describe '#save' do
let(:geocoding) { FactoryGirl.build(:geocoding, user: @user, user_table: @table) }
it 'validates formatter' do
geocoding.raise_on_save_failure = true
geocoding.formatter = nil
expect { geocoding.save }.to raise_error(Sequel::ValidationFailed)
geocoding.errors[:formatter].join(',').should match /is not present/
end
it 'validates kind' do
geocoding.raise_on_save_failure = true
geocoding.kind = 'nonsense'
expect { geocoding.save }.to raise_error(Sequel::ValidationFailed)
geocoding.errors[:kind].join(',').should match /is not in range or set/
end
it 'updates updated_at' do
geocoding = FactoryGirl.build(:geocoding, user: @user, user_table: @table, formatter: 'b', kind: 'admin0')
expect { geocoding.save }.to change(geocoding, :updated_at)
end
end
describe '#translate_formatter' do
let(:geocoding) { FactoryGirl.build(:geocoding, user: @user, user_table: @table) }
it 'translates a string with field names' do
geocoding.formatter = '{a}, {b}'
geocoding.translate_formatter.should == "a, ', ', b"
end
it 'translates a string with literals' do
geocoding.formatter = 'c'
geocoding.translate_formatter.should == "'c'"
end
it 'translates a string with mixed literals and field names' do
geocoding.formatter = '{a}, b, {c}'
geocoding.translate_formatter.should == "a, ', b, ', c"
end
end
describe '#run!' do
it 'marks the geocoding as failed if the geocoding job fails' do
geocoding = FactoryGirl.build(:geocoding, user: @user, formatter: 'a',
user_table: @table, geometry_type: 'polygon',
kind: 'admin0')
geocoding.class.stubs(:processable_rows).returns 10
CartoDB::TableGeocoder.any_instance.stubs(:used_batch_request?).returns false
CartoDB::TableGeocoder.any_instance.stubs(:run).raises("Error")
CartoDB.expects(:notify_exception).times(1)
geocoding.run!
geocoding.state.should eq 'failed'
end
it 'sends a payload with duration information' do
def is_metrics_payload?(str)
payload = JSON.parse(str)
payload.key?('queue_time') && payload.key?('processing_time') &&
payload['queue_time'] > 0 && payload['processing_time'] > 0
rescue JSON::ParserError
false
end
geocoding = FactoryGirl.create(:geocoding, user: @user, user_table: @table, kind: 'admin0',
geometry_type: 'polygon', formatter: 'b')
geocoding.class.stubs(:processable_rows).returns 10
CartoDB::InternalGeocoder::Geocoder.any_instance.stubs(:run).returns true
CartoDB::InternalGeocoder::Geocoder.any_instance.stubs(:process_results).returns true
CartoDB::InternalGeocoder::Geocoder.any_instance.stubs(:update_geocoding_status).returns(processed_rows: 10, state: 'completed')
# metrics_payload is sent to the log in json
Logger.any_instance.expects(:info).once.with { |str| is_metrics_payload?(str) }
Logger.any_instance.stubs(:info).with { |str| !is_metrics_payload?(str) }
geocoding.run!
geocoding.reload.state.should eq 'finished'
end
describe 'cartodb_georef_status interactions' do
before(:each) do
@my_table = FactoryGirl.create(:user_table, user_id: @user.id)
end
after(:each) do
@my_table.destroy
end
it 'marks rows to geocode with cartodb_georef_status = null' do
geocoding = FactoryGirl.build(:geocoding, user: @user, user_table: @my_table, kind: 'admin0', geometry_type: 'polygon', formatter: 'b')
geocoding.stubs(:run_geocoding!)
@my_table.service.add_column!(name: 'cartodb_georef_status', type: 'bool')
@my_table.service.insert_row!(cartodb_georef_status: nil)
@my_table.service.insert_row!(cartodb_georef_status: true)
@my_table.service.insert_row!(cartodb_georef_status: false)
geocoding.run!
@my_table.service.sequel.where(cartodb_georef_status: nil).count.should == 2
end
it 'sets cartodb_georef_status to null on all rows if force_all_rows=true' do
geocoding = FactoryGirl.build(:geocoding, user: @user, user_table: @my_table, kind: 'admin0', geometry_type: 'polygon', formatter: 'b', force_all_rows: true)
geocoding.stubs(:run_geocoding!)
@my_table.service.add_column!(name: 'cartodb_georef_status', type: 'bool')
@my_table.service.insert_row!(cartodb_georef_status: nil)
@my_table.service.insert_row!(cartodb_georef_status: true)
@my_table.service.insert_row!(cartodb_georef_status: false)
geocoding.run!
@my_table.service.sequel.where(cartodb_georef_status: nil).count.should == 3
end
end
it 'succeeds if there are no rows to geocode' do
geocoding = FactoryGirl.build(:geocoding, user: @user, formatter: 'a',
user_table: @table, geometry_type: 'polygon',
kind: 'admin0')
geocoding.class.stubs(:processable_rows).returns 0
geocoding.run!
geocoding.processed_rows.should eq 0
geocoding.state.should eq 'finished'
geocoding.cache_hits.should eq 0
geocoding.used_credits.should eq 0
end
pending 'raises an exception if the geocoding times out' do
geocoding = FactoryGirl.create(:geocoding, user: @user, user_table: @table, formatter: 'b', kind: 'high-resolution')
geocoding.class.stubs(:processable_rows).returns 10
geocoding.stubs(:processing_timeout_seconds).returns 0.01 # set timeout to 10 ms
table_geocoder_mock = mock
table_geocoder_mock.stubs(:run).with() { sleep(5) } # force it to sleep beyond timeout
geocoding.stubs(:table_geocoder).returns(table_geocoder_mock)
geocoding.run!
geocoding.reload.state.should eq 'failed'
end
it 'sends a track event through hubspot client' do
hubspot_instance = CartoDB::Hubspot.instance
hubspot_instance.expects(:track_geocoding_success).once.with() { |payload|
payload[:email] == @user.email && payload[:processed_rows] == 0
}
geocoding = FactoryGirl.build(:geocoding, user: @user, formatter: 'a',
user_table: @table, geometry_type: 'polygon',
kind: 'admin0')
geocoding.class.stubs(:processable_rows).returns 0
geocoding.run!
end
end
describe '#max_geocodable_rows' do
let(:geocoding) { FactoryGirl.build(:geocoding, user: @user) }
it 'returns the remaining quota if the user has hard limit' do
@user.stubs('hard_geocoding_limit?').returns(true)
delete_user_data @user
geocoding.max_geocodable_rows.should eq 200
user_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(@user.username, nil)
user_geocoder_metrics.incr(:geocoder_here, :success_responses, 100)
geocoding.max_geocodable_rows.should eq 100
end
it 'returns 50000 if the user has soft limit' do
@user.stubs('soft_geocoding_limit?').returns(true)
user_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(@user.username, nil)
user_geocoder_metrics.incr(:geocoder_here, :success_responses, 100)
geocoding.max_geocodable_rows.should eq 50000
end
it 'returns the remaining quota for the organization if the user has hard limit and belongs to an org' do
organization = create_organization_with_users(geocoding_quota: 150)
organization.owner.geocoder_provider = 'heremaps'
organization.owner.save.reload
org_user = organization.users.last
org_user.stubs('soft_geocoding_limit?').returns(false)
org_geocoding = FactoryGirl.build(:geocoding, user: org_user)
organization.geocoding_quota.should eq 150
org_geocoding.max_geocodable_rows.should eq 150
org_user_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(org_user.username, organization.name)
org_user_geocoder_metrics.incr(:geocoder_here, :success_responses, 100)
org_geocoding.max_geocodable_rows.should eq 50
organization.destroy
end
end
describe '#cancel' do
let(:geocoding) { FactoryGirl.build(:geocoding, user: @user, user_table: @table, formatter: 'foo', kind: 'high-resolution') }
it 'cancels the geocoding job' do
geocoding.table_geocoder.expects(:cancel).times(1).returns(true)
geocoding.cancel
end
it 'tries 5 times to cancel the geocoding job if that fails and after that sends an error report' do
CartoDB.expects(:notify_exception).times(1)
geocoding.table_geocoder.expects(:cancel).times(5).raises("error")
geocoding.cancel
end
end
describe '#calculate_used_credits' do
before(:each) do
@user.geocodings_dataset.delete
end
it 'returns 0 when the geocode is not high-resolution' do
geocoding = FactoryGirl.create(:geocoding, user: @user, kind: 'admin0', processed_rows: 10500, formatter: 'foo')
geocoding.calculate_used_credits.should eq 0
end
it 'returns 0 when the user has enough quota' do
# User has 200 geocoding credits, so geocoding 200 strings should take 0 credits
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 0, cache_hits: 200, kind: 'high-resolution', formatter: 'foo')
geocoding.calculate_used_credits.should eq 0
end
it 'returns the used credits when the user is over geocoding quota' do
redis_mock = MockRedis.new
user_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(@user.username, _org = nil, _redis = redis_mock)
CartoDB::GeocoderUsageMetrics.stubs(:new).returns(user_geocoder_metrics)
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 0, cache_hits: 100, kind: 'high-resolution', geocoder_type: 'heremaps', formatter: 'foo')
user_geocoder_metrics.incr(:geocoder_here, :success_responses, 100)
# 100 total (user has 200) => 0 used credits
geocoding.calculate_used_credits.should eq 0
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 0, cache_hits: 150, kind: 'high-resolution', geocoder_type: 'heremaps', formatter: 'foo')
user_geocoder_metrics.incr(:geocoder_cache, :success_responses, 150)
# 250 total => 50 used credits
geocoding.calculate_used_credits.should eq 50
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 100, cache_hits: 0, kind: 'high-resolution', geocoder_type: 'heremaps', formatter: 'foo')
user_geocoder_metrics.incr(:geocoder_here, :success_responses, 100)
# 350 total => 100 used credits
geocoding.calculate_used_credits.should eq 100
end
end
describe '#price' do
it 'returns 0 when the job used no credits' do
geocoding = FactoryGirl.create(:geocoding, user: @user, used_credits: 0, formatter: 'foo', kind: 'admin0')
geocoding.price.should eq 0
end
it 'returns the expected price when the geocoding used some credits' do
geocoding = FactoryGirl.create(:geocoding, user: @user, used_credits: 100, formatter: 'foo', kind: 'high-resolution')
geocoding.price.should eq 150
geocoding = FactoryGirl.create(:geocoding, user: @user, used_credits: 3, formatter: 'foo', kind: 'high-resolution')
geocoding.price.should eq 4.5
end
end
describe 'self.processable_rows' do
before(:each) do
@my_table = FactoryGirl.create(:user_table, user_id: @user.id)
end
after(:each) do
@my_table.destroy
end
it "returns all rows if there's no cartodb_georef_status column" do
3.times { @my_table.service.insert_row!({}) }
Geocoding.processable_rows(@my_table.service).should == 3
end
it "returns all rows where cartodb_georef_status <> true" do
@my_table.service.add_column!(name: 'cartodb_georef_status', type: 'bool')
3.times { @my_table.service.insert_row!(cartodb_georef_status: nil) }
@my_table.service.insert_row!(cartodb_georef_status: true)
@my_table.service.insert_row!(cartodb_georef_status: false)
Geocoding.processable_rows(@my_table.service).should == 4
end
it "returns all rows regardless of cartodb_georef_status if force_all_rows=true" do
@my_table.service.add_column!(name: 'cartodb_georef_status', type: 'bool')
3.times { @my_table.service.insert_row!(cartodb_georef_status: nil) }
@my_table.service.insert_row!(cartodb_georef_status: true)
@my_table.service.insert_row!(cartodb_georef_status: false)
Geocoding.processable_rows(@my_table.service, true).should == 5
end
end
describe '#failed_rows and #successful_rows' do
before(:each) do
@user.geocodings_dataset.delete
end
it 'returns expected results' do
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 0, cache_hits: 100, real_rows: 100, processable_rows: 100, kind: 'high-resolution', formatter: 'foo')
geocoding.failed_rows.should eq 0
geocoding.successful_rows.should eq 100
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 10, cache_hits: 150, real_rows: 155, processable_rows: 160, kind: 'high-resolution', formatter: 'foo')
geocoding.failed_rows.should eq 5
geocoding.successful_rows.should eq 155
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 100, cache_hits: 0, real_rows: 100, processable_rows: 100, kind: 'high-resolution', formatter: 'foo')
geocoding.failed_rows.should eq 0
geocoding.successful_rows.should eq 100
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 0, cache_hits: 0, real_rows: 0, processable_rows: 0, kind: 'high-resolution', formatter: 'foo')
geocoding.failed_rows.should eq 0
geocoding.successful_rows.should eq 0
geocoding = FactoryGirl.create(:geocoding, user: @user, processed_rows: 100, cache_hits: 0, real_rows: 0, processable_rows: 100, kind: 'high-resolution', formatter: 'foo')
geocoding.failed_rows.should eq 100
geocoding.successful_rows.should eq 0
end
end
end