Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
require 'spec_helper'
describe AccessToken do
before(:all) do
puts "\n[rspec] Creating User for AccessToken spec..."
@user = create_user
end
after(:all) do
bypass_named_maps
@user.destroy
end
it "should store tokens in redis when it is created" do
client_application = @user.client_application
access_token = AccessToken.create(:user => @user, :client_application => client_application)
access_token.exists?
base_key = "rails:oauth_access_tokens:#{access_token.token}"
$api_credentials.hget(base_key, "consumer_key").should == client_application.key
$api_credentials.hget(base_key, "consumer_secret").should == client_application.secret
$api_credentials.hget(base_key, "access_token_token").should == access_token.token
$api_credentials.hget(base_key, "access_token_secret").should == access_token.secret
$api_credentials.hget(base_key, "user_id").should == @user.id.to_s
$api_credentials.hget(base_key, "time").should == access_token.authorized_at.to_s
end
it "should remove tokens from redis when it is destroyed" do
client_application = @user.client_application
access_token = AccessToken.create(:user => @user, :client_application => client_application)
access_token.exists?
base_key = "rails:oauth_access_tokens:#{access_token.token}"
$api_credentials.keys.should include(base_key)
access_token.destroy
$api_credentials.keys.should_not include(base_key)
end
end
+139
View File
@@ -0,0 +1,139 @@
require 'spec_helper'
describe Asset do
before(:all) do
@user = create_user
end
after(:all) do
bypass_named_maps
@user.destroy
end
describe '#validate' do
it 'validates user_id' do
asset = Asset.new
asset.valid?.should be_false
asset.errors.full_messages.should include("user_id can't be blank")
end
it 'validates file existence' do
asset = Asset.new user_id: @user.id, asset_file: (Rails.root + 'db/fake_data/i_dont_exist.png').to_s
asset.valid?.should be_false
asset.errors.full_messages.should include("file is invalid")
end
it 'validates file correct extension' do
asset = Asset.new user_id: @user.id, asset_file: (Rails.root + 'db/fake_data/i_dont_exist.json').to_s
asset.valid?.should be_false
asset.errors.full_messages.should include("file has invalid format")
end
it 'validates file correct metadata' do
asset = Asset.new user_id: @user.id, asset_file: (Rails.root + 'spec/support/data/fake_png.png').to_s
asset.valid?.should be_false
asset.errors.full_messages.should include("file doesn't appear to be an image")
end
it 'validates file size' do
asset = Asset.new user_id: @user.id, asset_file: (Rails.root + 'spec/support/data/images/pattern.jpg').to_s
asset.stubs(:max_size).returns(10)
asset.valid?.should be_false
asset.errors.full_messages.should include("file is too big, 0.0MB max")
end
it 'validates file dimensions' do
asset = Asset.new user_id: @user.id, asset_file: (Rails.root + 'spec/support/data/images/1025x1.jpg').to_s
asset.valid?.should be_false
asset.errors.full_messages.should include("file is too big, 1024x1024 max")
end
it 'validates urls' do
asset = Asset.new user_id: @user.id, url: "http://foo"
asset.valid?.should be_false
asset.errors.full_messages.should include("url is invalid")
end
end
def local_path(asset)
local_url = CGI.unescape(asset.public_url).gsub(/\/uploads/, '')
Carto::Conf.new.public_uploaded_assets_path + local_url
end
describe '#create' do
describe 'on local filesystem' do
before(:each) do
Asset.any_instance.stubs("use_s3?").returns(false)
end
it 'should save the file when passing a full path as an argument' do
asset = Asset.create user_id: @user.id, asset_file: (Rails.root + 'spec/support/data/cartofante_blue.png').to_s
File.exists?(local_path(asset)).should be_true
asset.public_url.should =~ /.*test\/#{@user.username}\/assets\/\d+cartofante_blue\.png.*/
end
it 'should save the file when passing an UploadedFile as an argument' do
file_path = Rails.root.join('spec/support/data/cartofante_blue.png')
asset = Asset.create(
user_id: @user.id,
asset_file: Rack::Test::UploadedFile.new(file_path, 'image/png'))
File.exists?(local_path(asset)).should be_true
asset.public_url.should =~ /.*test\/#{@user.username}\/assets\/\d+cartofante_blue.png.*/
end
it 'should save the public url when passing it as an argument' do
file = Rails.root.join('spec/support/data/cartofante_blue.png')
serve_file file do |url|
asset = Asset.create(user_id: @user.id, public_url: url)
asset.public_url.should == url
end
end
it 'should download the file and move it to the public assets path when passing an url as an argument' do
file = Rails.root.join('spec/support/data/cartofante_blue.png')
serve_file file do |url|
asset = Asset.create(user_id: @user.id, url: url)
File.exists?(local_path(asset)).should be_true
asset.public_url.should =~ /\/test\/#{@user.username}\/assets\/\d+cartofante_blue\.png/
end
end
it 'should import assets with spaces in their name' do
asset = Asset.create user_id: @user.id, asset_file: (Rails.root + 'spec/support/data/cartofante blue.png').to_s
File.exists?(local_path(asset)).should be_true
asset.public_url.should =~ /\/test\/#{@user.username}\/assets\/\d+cartofante%20blue\.png/
end
end
end
describe '#destroy' do
it 'removes the file from storage if needed' do
Asset.any_instance.stubs("use_s3?").returns(false)
asset = Asset.create user_id: @user.id, asset_file: (Rails.root + 'spec/support/data/cartofante_blue.png').to_s
path = local_path(asset)
File.exists?(path).should be_true
asset.destroy
File.exists?(path).should be_false
end
it 'removes the file with special characters from storage' do
Asset.any_instance.stubs("use_s3?").returns(false)
asset = Asset.create user_id: @user.id, asset_file: (Rails.root + 'spec/support/data/cartofante blue.png').to_s
path = local_path(asset)
File.exists?(path).should be_true
asset.destroy
File.exists?(path).should be_false
end
end
describe '#presenter' do
it 'returns the expected format' do
asset = Asset.new user_id: @user.id, asset_file: (Rails.root + 'db/fake_data/simple.json').to_s
Carto::Api::AssetPresenter.new(asset).to_hash.should == { public_url: nil, user_id: @user.id, id: nil, kind: nil }
end
end
end
+49
View File
@@ -0,0 +1,49 @@
require 'spec_helper_min'
require 'support/helpers'
require 'helpers/account_types_helper'
describe Carto::AccountType do
include CartoDB::Factories
include AccountTypesHelper
before :each do
@account_type = create_account_type_fg('PRO')
end
after :each do
@account_type.destroy if @account_type
end
describe '#create' do
it 'is persisted correctly to database' do
account_type = Carto::AccountType.find(@account_type.account_type)
account_type.should_not be_nil
account_type.rate_limit.should_not be_nil
end
it 'destroys an account_type' do
account_type = Carto::AccountType.find(@account_type.account_type)
account_type.destroy
expect {
Carto::AccountType.find(@account_type.account_type)
}.to raise_error(ActiveRecord::RecordNotFound)
expect {
Carto::RateLimit.find(@account_type.rate_limit_id)
}.to raise_error(ActiveRecord::RecordNotFound)
end
it 'updates the rate limits of an account_type' do
account_type = Carto::AccountType.find(@account_type.account_type)
rate_limits = FactoryGirl.create(:rate_limits)
account_type.rate_limit = rate_limits
account_type.save
updated_account_type = Carto::AccountType.find(@account_type.account_type)
updated_account_type.rate_limit.id.should eq rate_limits.id
end
end
end
+87
View File
@@ -0,0 +1,87 @@
require 'spec_helper_min'
describe Carto::AnalysisNode do
let(:analysis_definition) do
{
id: "a2",
type: "point-in-polygon",
options: { primary_source_name: "polygons_source" },
params: {
points_source: {
id: "paradas_metro_madrid",
type: "source",
params: { query: "SELECT * FROM paradas_metro_madrid" },
options: { table_name: "paradas_metro_madrid" }
},
polygons_source: {
id: "a1",
type: "buffer",
options: { kind: "car", time: "100", distance: "kilometers" },
params: {
source: {
id: "a0",
type: "source",
params: { query: "SELECT * FROM paradas_metro_madrid" },
options: { table_name: "paradas_metro_madrid" }
},
radius: 2000,
isolines: 1,
dissolved: false
}
}
}
}
end
before(:each) do
@node = Carto::AnalysisNode.new(analysis_definition)
end
it 'returns definition values from accessors' do
@node.id.should eq analysis_definition[:id]
@node.type.should eq analysis_definition[:type]
@node.params.should eq analysis_definition[:params]
@node.options.should eq analysis_definition[:options]
end
describe '#children' do
it 'returns all children of a node' do
@node.children.count.should eq 2
@node.children[0].id.should eq 'paradas_metro_madrid'
@node.children[1].id.should eq 'a1'
end
it 'returns an empty list when no childrens are found' do
@node.definition.delete(:params)
@node.children.should eq []
end
end
describe '#find_by_id' do
it 'finds root element' do
@node.find_by_id('a2').should eq @node
end
it 'finds direct children' do
found = @node.find_by_id('a1')
found.should be
end
it 'finds indirect descendants' do
found = @node.find_by_id('a0')
found.should be
end
it 'returns nil if not found' do
@node.find_by_id('404').should be_nil
end
end
describe '#source_descendants' do
it 'find all descendant of a node of type source' do
sources = @node.source_descendants
sources.count.should eq 2
sources.all?(&:source?).should be_true
end
end
end
+230
View File
@@ -0,0 +1,230 @@
require 'spec_helper_min'
require 'support/helpers'
describe Carto::Analysis do
let(:definition_with_options) do
{
id: "a1",
type: "buffer",
params: {
distance: 100
},
options: {
unit: "m"
}
}
end
let(:nested_definition_with_options) do
{
id: "a1",
type: "buffer",
params: {
source: definition_with_options
}
}
end
let(:point_in_polygon_definition_with_options) do
{
id: "a2",
type: "point-in-polygon",
options: { primary_source_name: "polygons_source" },
params: {
polygon_source: definition_with_options,
points_source: {
id: "table",
type: "source",
params: { query: "SELECT * FROM table" },
options: { table_name: "table" }
}
}
}
end
describe '#natural_id' do
it 'returns nil if analysis definition has no id at the first level' do
Carto::Analysis.new(analysis_definition: nil).natural_id.should eq nil
Carto::Analysis.new(analysis_definition: {}).natural_id.should eq nil
Carto::Analysis.new(analysis_definition: { "wadus" => 1 }).natural_id.should eq nil
Carto::Analysis.new(analysis_definition: { wadus: 1 }).natural_id.should eq nil
end
it 'returns id if analysis definition has id at the first level' do
Carto::Analysis.new(analysis_definition: { id: "a1" }).natural_id.should eq 'a1'
end
end
describe '#analysis_definition' do
it 'removes options from analysis definition' do
analysis = Carto::Analysis.new(analysis_definition: definition_with_options)
analysis.analysis_definition.include?(:options).should be_true
analysis.analysis_definition_for_api.include?(:options).should be_false
end
it 'removes options from nested source analysis' do
analysis = Carto::Analysis.new(analysis_definition: nested_definition_with_options)
nested_analysis = analysis.analysis_definition_for_api[:params][:source]
nested_analysis.include?(:options).should be_false
end
it 'removes options from nested source analysis with multiple sources' do
analysis = Carto::Analysis.new(analysis_definition: point_in_polygon_definition_with_options)
polygon_analysis = analysis.analysis_definition_for_api[:params][:polygon_source]
polygon_analysis.include?(:options).should be_false
points_analysis = analysis.analysis_definition_for_api[:params][:points_source]
points_analysis.include?(:options).should be_false
end
end
describe '#analysis_node' do
it 'returns an analysis node with the definition of the analysis' do
analysis = Carto::Analysis.new(analysis_definition: definition_with_options)
node = analysis.analysis_node
node.definition.should eq analysis.analysis_definition
node.children.should be_empty
end
it 'returns an analysis tree' do
analysis = Carto::Analysis.new(analysis_definition: nested_definition_with_options)
root = analysis.analysis_node
root.definition.should eq analysis.analysis_definition
children = root.children
children.should_not be_empty
children[0].children.should be_empty
children[0].definition.should eq definition_with_options
end
end
describe '#save' do
include Carto::Factories::Visualizations
include HelperMethods
before(:all) do
bypass_named_maps
@user = FactoryGirl.create(:carto_user)
@map, @table, @table_visualization, @visualization = create_full_visualization(@user)
@analysis = FactoryGirl.create(:source_analysis, visualization_id: @visualization.id, user_id: @user.id)
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
# This avoids connection leaking.
::User[@user.id].destroy
@analysis.destroy
end
it 'triggers notify_map_change on related map(s)' do
map = mock
map.stubs(:id).returns(@map.id)
map.stubs(:data_layers).returns([])
map.expects(:update_dataset_dependencies).once
map.expects(:notify_map_change).once
Map.stubs(:where).with(id: map.id).returns([map])
@analysis.stubs(:map).returns(map)
@analysis.analysis_definition = definition_with_options
@analysis.save
end
end
context 'viewer users' do
include Carto::Factories::Visualizations
include HelperMethods
before(:all) do
@user = FactoryGirl.create(:carto_user)
@map, @table, @table_visualization, @visualization = create_full_visualization(@user)
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
# This avoids connection leaking.
::User[@user.id].destroy
@analysis.destroy if @analysis
end
before(:each) do
bypass_named_maps
end
after(:each) do
@user.viewer = false
@user.save
end
it "can't create a new analysis" do
@user.viewer = true
@user.save
@analysis = FactoryGirl.build(:source_analysis, visualization_id: @visualization.id, user_id: @user.id)
@analysis.save.should be_false
@analysis.errors[:user].should eq(["Viewer users can't edit analyses"])
end
it "can't delete analyses" do
@analysis = FactoryGirl.create(:source_analysis, visualization_id: @visualization.id, user_id: @user.id)
@user.viewer = true
@user.save
@analysis.reload
@analysis.destroy.should eq false
Carto::Analysis.exists?(@analysis.id).should eq true
@analysis.errors[:user].should eq(["Viewer users can't edit analyses"])
end
end
describe '#source_analysis_for_layer' do
include Carto::Factories::Visualizations
before(:all) do
@user = FactoryGirl.create(:carto_user)
@map, @table, @table_visualization, @visualization = create_full_visualization(@user)
@layer = @visualization.data_layers.first
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
# This avoids connection leaking.
::User[@user.id].destroy
end
it 'copies the layer query' do
@layer.options[:query] = 'SELECT * FROM wadus'
analysis = Carto::Analysis.source_analysis_for_layer(@layer, 0)
analysis.analysis_node.params[:query].should eq 'SELECT * FROM wadus'
end
it 'copies the layer table_name' do
@layer.options[:table_name] = 'tt11'
analysis = Carto::Analysis.source_analysis_for_layer(@layer, 0)
analysis.analysis_node.options[:table_name].should eq 'tt11'
end
it 'copies only the table_name for non-org users' do
@layer.options.merge!(table_name: 'tt11', user_name: 'juan')
analysis = Carto::Analysis.source_analysis_for_layer(@layer, 0)
analysis.analysis_node.options[:table_name].should eq 'tt11'
end
it 'always qualifies table_name in organizations' do
@layer.options.merge!(table_name: 'tt33', user_name: @user.username)
@layer.user.stubs(:organization_user?).returns(true)
analysis = Carto::Analysis.source_analysis_for_layer(@layer, 0)
analysis.analysis_node.options[:table_name].should eq "#{@user.username}.tt33"
end
it 'uses default SQL query if missing' do
@layer.options.merge!(table_name: 'tt33', user_name: @user.username, query: '')
Carto::User.any_instance.stubs(:organization_user?).returns(true)
analysis = Carto::Analysis.source_analysis_for_layer(@layer, 0)
analysis.analysis_node.params[:query].should eq "SELECT * FROM #{@user.username}.tt33"
end
end
end
File diff suppressed because it is too large Load Diff
+189
View File
@@ -0,0 +1,189 @@
require 'spec_helper_min'
require 'helpers/storage_helper'
require 'helpers/subdomainless_helper'
describe Carto::Asset do
# Needed so subdomainless_helper works
def host!(_) end
before(:all) do
@organization = Carto::Organization.find(FactoryGirl.create(:organization).id)
@user = FactoryGirl.create(:carto_user)
@visualization = FactoryGirl.create(:carto_visualization, user: @user)
end
after(:all) do
@visualization.destroy
@organization.destroy
@user.destroy
end
let(:storage_info) do
{
type: 's3',
location: 'manolo_subfolder',
identifier: 'could_be_a_manolo_hash_23as4g5sh6sd7hd8j9jfgk'
}
end
let(:public_url) do
'https://manolo.es/es/co/bar'
end
describe('#.for_organization') do
include StorageHelper
it 'should create a valid asset for organization and resource' do
bypass_storage
asset = Carto::Asset.for_organization(organization: @organization,
resource: Tempfile.new(['manolo', '.jpg']))
asset.valid?.should be_true
end
end
describe('#destroy') do
it 'doesn\'t try to remove from storage if no storage_info is present' do
Carto::AssetsService.any_instance.expects(:remove).never
Carto::Asset.create(user: @user).destroy
end
it 'removes asset from storage if storage_info is present' do
Carto::AssetsService.any_instance.expects(:remove).once
Carto::Asset.create(user: @user, storage_info: storage_info).destroy
end
end
describe('#absolute_public_url') do
let(:asset) { Carto::Asset.new(organization: @organization, public_url: '/uploads/wadus') }
it 'preprends subdomain name in subdomainful' do
stub_domainful(@organization.name)
domain = "http://#{@organization.name}.localhost.lan#{CartoDB.http_port}"
asset.absolute_public_url.should eq domain + asset.public_url
end
it 'preprends domain name in subdomainless' do
stub_subdomainless
domain = "http://localhost.lan#{CartoDB.http_port}"
asset.absolute_public_url.should eq domain + asset.public_url
end
end
describe('#validation') do
it 'requires a user or an organization' do
asset = Carto::Asset.new
asset.valid?.should be_false
asset.errors[:user].should_not be_empty
asset.errors[:organization].should_not be_empty
asset.errors[:visualization].should_not be_empty
end
it 'requires a public url' do
asset = Carto::Asset.new(organization: @organization,
storage_info: storage_info)
asset.valid?.should be_false
asset.errors[:public_url].should_not be_empty
end
describe('#user asset') do
it 'accepts good asset' do
asset = Carto::Asset.new(user: @user, public_url: public_url)
asset.valid?.should be_true
end
it 'accepts nil storage_info' do
asset = Carto::Asset.new(user: @user, public_url: public_url)
asset.valid?.should be_true
end
it 'rejects incomplete storage_info' do
storage_info.delete(:type)
asset = Carto::Asset.new(user: @user,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
end
it 'rejects spammy storage_info' do
storage_info[:great_idea] = 'to spam a json!'
asset = Carto::Asset.new(user: @user,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
end
end
describe('#organization asset') do
it 'accepts good asset' do
asset = Carto::Asset.new(organization: @organization,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_true
end
it 'rejects nil storage_info' do
asset = Carto::Asset.new(organization: @organization,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
asset.errors[:storage_info].should eq ["can't be blank"]
end
it 'rejects incomplete storage_info' do
storage_info.delete(:type)
asset = Carto::Asset.new(organization: @organization,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
end
it 'rejects spammy storage_info' do
storage_info[:great_idea] = 'to spam a json!'
asset = Carto::Asset.new(organization: @organization,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
end
end
describe('#visualization asset') do
it 'accepts good asset' do
asset = Carto::Asset.new(visualization: @visualization,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_true
end
it 'rejects nil storage_info' do
asset = Carto::Asset.new(visualization: @visualization,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
asset.errors[:storage_info].should eq ["can't be blank"]
end
it 'rejects incomplete storage_info' do
storage_info.delete(:type)
asset = Carto::Asset.new(visualization: @visualization,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
end
it 'rejects spammy storage_info' do
storage_info[:great_idea] = 'to spam a json!'
asset = Carto::Asset.new(visualization: @visualization,
storage_info: storage_info,
public_url: public_url)
asset.valid?.should be_false
asset.errors[:storage_info].should_not be_empty
end
end
end
end
+7
View File
@@ -0,0 +1,7 @@
require 'spec_helper_min'
require_relative '../data_import_shared_examples'
describe Carto::DataImport do
let(:data_import_class) { Carto::DataImport }
it_behaves_like 'DataImport model'
end
+28
View File
@@ -0,0 +1,28 @@
require_relative '../../spec_helper_min'
describe Carto::FeatureFlag do
before(:all) do
@user = FactoryGirl.create(:carto_user)
@user_ff = FactoryGirl.create(:carto_feature_flag, name: 'user-ff', restricted: true)
@unused_ff = FactoryGirl.create(:carto_feature_flag, name: 'unused-ff', restricted: true)
FactoryGirl.create(:feature_flags_user, feature_flag_id: @user_ff.id, user_id: @user.id)
@unrestricted_ff = FactoryGirl.create(:carto_feature_flag, name: 'unrestricted-ff', restricted: false)
end
after(:all) do
@user.destroy
[@user_ff, @unused_ff, @unrestricted_ff].each(&:destroy)
end
describe "#find_by_user" do
it "returns the user feature flags and the unrestricted ones" do
expected_result = [@unrestricted_ff, @user_ff]
result = Carto::FeatureFlag.find_by_user(@user)
expect(result).to eql(expected_result)
end
end
end
+50
View File
@@ -0,0 +1,50 @@
require_relative '../../spec_helper'
require_relative '../../../app/models/carto/group'
describe Carto::Group do
describe '#valid_group_name' do
it 'returns valid names' do
valid_names = ['A Group', 'My5Group']
valid_names.each do |valid_name|
Carto::Group.valid_group_name(valid_name).should == valid_name
end
end
it 'returns g_ prepended at the name if does not begin with a letter' do
not_valid_names = { '5 group' => 'g_5 group' }
not_valid_names.each do |name, valid|
Carto::Group.valid_group_name(name).should == valid
end
end
it 'changes not alphanumeric or spaces characters to underscores' do
not_valid_names = { '/group' => 'g_group', 'g/group' => 'g_group' }
not_valid_names.each do |name, valid|
Carto::Group.valid_group_name(name).should == valid
end
end
end
describe 'organization behaviour' do
include_context 'organization with users helper'
include Carto::Factories::Visualizations
before(:all) do
@group = FactoryGirl.create(:random_group, organization_id: @organization.id)
end
after(:all) do
@group.destroy
end
it 'generates auth_tokens and save them for future accesses' do
token = @group.get_auth_token
token.should be
@group.reload
@group.get_auth_token.should eq token
end
end
end
@@ -0,0 +1,137 @@
require_relative '../../../../app/models/carto/helpers/billing_cycle'
require 'ostruct'
require 'delorean'
describe Carto::BillingCycle do
describe "#last_billing_cycle" do
class MyDummyUser < OpenStruct
include Carto::BillingCycle
end
before(:each) do
@user = MyDummyUser.new
end
[
{ today: '2017-03-21', period_end_date: '2017-02-21', expected: '2017-03-21' },
{ today: '2017-03-21', period_end_date: '2017-02-01', expected: '2017-03-01' },
{ today: '2017-02-21', period_end_date: '2017-01-01', expected: '2017-02-01' },
{ today: '2017-01-31', period_end_date: '2016-10-31', expected: '2017-01-31' },
{ today: '2017-03-01', period_end_date: '2016-10-01', expected: '2017-03-01' }
].each do |example|
today = example[:today]
period_end_date = example[:period_end_date]
expected = example[:expected]
it "returns the current month if period_end_date.day <= today.day" \
" (today = #{today}, period_end_date = #{period_end_date}, expected = #{expected})" do
@user.period_end_date = Date.parse(period_end_date)
Delorean.time_travel_to(today) do
@user.last_billing_cycle.should eq Date.parse(expected)
end
end
end
[
{ today: '2017-03-20', period_end_date: '2017-02-21', expected: '2017-02-21' },
{ today: '2017-03-01', period_end_date: '2017-02-21', expected: '2017-02-21' },
{ today: '2017-02-01', period_end_date: '2017-01-21', expected: '2017-01-21' },
{ today: '2017-01-30', period_end_date: '2016-10-31', expected: '2016-12-31' },
{ today: '2017-03-01', period_end_date: '2016-10-02', expected: '2017-02-02' }
].each do |example|
today = example[:today]
period_end_date = example[:period_end_date]
expected = example[:expected]
it "returns the previous month if period_end_date.day > today.day" \
" (today = #{today}, period_end_date = #{period_end_date}, expected = #{expected})" do
@user.period_end_date = Date.parse(period_end_date)
Delorean.time_travel_to(today) do
@user.last_billing_cycle.should eq Date.parse(expected)
end
end
end
[
{ today: '2017-03-21', period_end_date: '2017-01-31', expected: '2017-02-28' },
# 2020 is a leap year
{ today: '2020-03-21', period_end_date: '2017-01-31', expected: '2020-02-29' },
{ today: '2017-05-21', period_end_date: '2017-01-31', expected: '2017-04-30' }
].each do |example|
today = example[:today]
period_end_date = example[:period_end_date]
expected = example[:expected]
it "returns the previous valid day when dealing with 28, 29 or 30-day month corner cases" \
" (today = #{today}, period_end_date = #{period_end_date}, expected = #{expected})" do
@user.period_end_date = Date.parse(period_end_date)
Delorean.time_travel_to(today) do
@user.last_billing_cycle.should eq Date.parse(expected)
end
end
end
end
describe "#next_billing_cycle" do
class MyDummyUser < OpenStruct
include Carto::BillingCycle
end
before(:each) do
@user = MyDummyUser.new
end
[
{ today: '2017-03-10', period_end_date: '2017-02-21', expected: '2017-03-21' },
{ today: '2017-03-10', period_end_date: '2017-07-31', expected: '2017-03-31' },
{ today: '2017-03-10', period_end_date: '2017-01-11', expected: '2017-03-11' },
{ today: '2017-01-10', period_end_date: '2016-10-31', expected: '2017-01-31' },
{ today: '2017-03-01', period_end_date: '2016-10-02', expected: '2017-03-02' }
].each do |example|
today = example[:today]
period_end_date = example[:period_end_date]
expected = example[:expected]
it "returns the current month if period_end_date.day > today.day" \
" (today = #{today}, period_end_date = #{period_end_date}, expected = #{expected})" do
@user.period_end_date = Date.parse(period_end_date)
Delorean.time_travel_to(today) do
@user.next_billing_cycle.should eq Date.parse(expected)
end
end
end
[
{ today: '2017-03-01', period_end_date: '2016-10-01', expected: '2017-04-01' },
{ today: '2017-03-10', period_end_date: '2017-02-01', expected: '2017-04-01' },
{ today: '2017-02-01', period_end_date: '2017-02-01', expected: '2017-03-01' },
{ today: '2017-01-18', period_end_date: '2016-10-15', expected: '2017-02-15' },
{ today: '2017-03-12', period_end_date: '2015-07-04', expected: '2017-04-04' }
].each do |example|
today = example[:today]
period_end_date = example[:period_end_date]
expected = example[:expected]
it "returns the next month if period_end_date.day <= today.day" \
" (today = #{today}, period_end_date = #{period_end_date}, expected = #{expected})" do
@user.period_end_date = Date.parse(period_end_date)
Delorean.time_travel_to(today) do
@user.next_billing_cycle.should eq Date.parse(expected)
end
end
end
[
{ today: '2017-03-21', period_end_date: '2017-01-31', expected: '2017-03-31' },
# 2020 is a leap year
{ today: '2020-02-21', period_end_date: '2017-01-31', expected: '2020-02-29' },
{ today: '2017-04-21', period_end_date: '2017-01-31', expected: '2017-04-30' }
].each do |example|
today = example[:today]
period_end_date = example[:period_end_date]
expected = example[:expected]
it "returns the previous valid day when dealing with 28, 29 or 30-day month corner cases" \
" (today = #{today}, period_end_date = #{period_end_date}, expected = #{expected})" do
@user.period_end_date = Date.parse(period_end_date)
Delorean.time_travel_to(today) do
@user.next_billing_cycle.should eq Date.parse(expected)
end
end
end
end
end
+102
View File
@@ -0,0 +1,102 @@
require_relative '../../spec_helper'
require_relative '../../factories/organizations_contexts'
require_relative '../../../app/models/carto/invitation'
require_relative '../../../app/controllers/carto/api/user_creations_controller'
require_relative '../../../lib/resque/user_jobs'
describe Carto::Invitation do
include_context 'organization with users helper'
describe 'creation' do
it 'fails for existing users' do
invitation = Carto::Invitation.create_new(@carto_org_user_owner, [@carto_org_user_1.email], 'hi', false)
invitation.valid?.should == false
end
it 'fails for non-owner users' do
expect do
Carto::Invitation.create_new(@carto_org_user_1, ['no@carto.com'], 'hi', false)
end.to raise_error CartoDB::InvalidUser
end
it 'fails for wrong emails' do
invitation = Carto::Invitation.create_new(@carto_org_user_owner, ['no', 'neither@'], 'hi', false)
invitation.valid?.should == false
invitation.errors[:users_emails].count.should == 2
end
it 'sends invitations' do
::Resque.expects(:enqueue).with(Resque::OrganizationJobs::Mail::Invitation, instance_of(String)).once
emails = ['w_1@carto.com', 'w_2@carto.com']
invitation = Carto::Invitation.create_new(@carto_org_user_owner, emails, 'hi', false)
invitation.inviter_user_id.should == @carto_org_user_owner.id
invitation.organization_id.should == @carto_org_user_owner.organization_id
end
end
describe 'token' do
before(:each) do
@invitation = Carto::Invitation.create_new(@carto_org_user_owner, [], 'Welcome!', false)
@invitation_2 = Carto::Invitation.create_new(@carto_org_user_owner, [], 'Welcome!', false)
end
it 'returns the same token for the same email' do
email = 'myemail@carto.com'
t1 = @invitation.token(email)
t2 = @invitation.token(email)
t1.should == t2
end
it 'returns different tokens for different emails' do
t1 = @invitation.token('email1@carto.com')
t2 = @invitation.token('email2@carto.com')
t1.should_not == t2
end
it 'returns different tokens using the same email with different invitations' do
email = 'myemail@carto.com'
t1 = @invitation.token(email)
t2 = @invitation_2.token(email)
t1.should_not == t2
end
it 'has a length > 10' do
@invitation.token('email1@carto.com').length.should > 10
end
end
describe '#use' do
before(:each) do
@valid_email = 'email1@carto.com'
@valid_email_2 = 'email2@carto.com'
@invitation = Carto::Invitation.create_new(
@carto_org_user_owner,
[@valid_email, @valid_email_2],
'Welcome!',
false
)
@token = @invitation.token(@valid_email)
@token_2 = @invitation.token(@valid_email_2)
end
it 'return true for valid emails + token' do
@invitation.use(@valid_email, @token).should == true
end
it 'return false for non valid emails' do
@invitation.use('fake@carto.com', @token).should == false
end
it 'return false for non valid tokens' do
@invitation.use(@valid_email, 'fake_token').should == false
end
it 'triggers an AlreadyUsedInvitationError if a user uses it twice' do
@invitation.use(@valid_email, @token)
@invitation.use(@valid_email_2, @token_2)
@invitation.reload
expect { @invitation.use(@valid_email, @token) }.to raise_error AlreadyUsedInvitationError
expect { @invitation.use(@valid_email_2, @token_2) }.to raise_error AlreadyUsedInvitationError
end
end
end
+159
View File
@@ -0,0 +1,159 @@
require 'spec_helper_min'
require 'models/layer_shared_examples'
describe Carto::Layer do
include Carto::Factories::Visualizations
it_behaves_like 'Layer model' do
let(:layer_class) { Carto::Layer }
def create_map(options = {})
options.delete(:table_id)
map = Carto::Map.create(options)
FactoryGirl.create(:carto_visualization, map: map, user_id: options[:user_id]) if options[:user_id].present?
map
end
def add_layer_to_entity(entity, layer)
entity.layers << layer
end
before(:all) do
@user = FactoryGirl.create(:carto_user, private_tables_enabled: true)
@table = Table.new
@table.user_id = @user.id
@table.save
end
before(:each) do
bypass_named_maps
end
after(:all) do
@user.destroy
end
describe '#copy' do
it 'returns a copy of the layer' do
layer = layer_class.create(kind: 'carto', options: { style: 'bogus' })
layer_copy = layer.dup
layer_copy.kind.should == layer.kind
layer_copy.options.should == layer.options
layer_copy.id.should be_nil
end
end
end
describe '#affected_tables' do
before(:all) do
bypass_named_maps
@user = FactoryGirl.create(:carto_user)
@map, @table1, @table_visualization, @visualization = create_full_visualization(@user)
@table2 = FactoryGirl.create(:carto_user_table, user_id: @user.id, map_id: @map.id)
@analysis = FactoryGirl.create(:analysis_point_in_polygon,
user: @user, visualization: @visualization,
source_table: @table1.name, target_table: @table2.name)
@layer = @map.data_layers.first
end
after(:all) do
@analysis.destroy
destroy_full_visualization(@map, @table1, @table_visualization, @visualization)
@user.destroy
end
let(:source_analysis_id) { @analysis.analysis_node.children[0].id }
let(:target_analysis_id) { @analysis.analysis_node.children[1].id }
it 'returns all tables from the root analysis' do
@layer.stubs(:options).returns(source: @analysis.natural_id)
@layer.stubs(:affected_table_names).returns([]).twice
affected = @layer.send(:affected_tables)
affected.count.should eq 2
affected.should include @table1
affected.should include @table2
end
it 'returns only tables from the sub-analysis' do
@layer.stubs(:options).returns(source: source_analysis_id)
@layer.stubs(:affected_table_names).returns([]).once
affected = @layer.send(:affected_tables)
affected.count.should eq 1
affected.should include @table1
@layer.stubs(:options).returns(source: target_analysis_id)
@layer.stubs(:affected_table_names).returns([]).once
affected = @layer.send(:affected_tables)
affected.count.should eq 1
affected.should include @table2
end
it 'ignores query/table_name if source is specified' do
@layer.stubs(:options).returns(source: 'wadus', table_name: @table1.name)
affected = @layer.send(:affected_tables)
affected.should be_empty
end
it 'fallbacks to query/table_name if source is not specified' do
@layer.stubs(:options).returns(table_name: @table1.name)
affected = @layer.send(:affected_tables)
affected.should eq [@table1]
query = "SELECT * FROM #{@table2.name}"
@layer.stubs(:options).returns(query: query)
@layer.stubs(:affected_table_names).with(query).returns([@table2.name]).once
affected = @layer.send(:affected_tables)
affected.should eq [@table2]
end
it 'returns values only from query (overrides table_name) if both specified' do
query = "SELECT * FROM #{@table2.name}"
@layer.stubs(:options).returns(table_name: @table1.name, query: query)
@layer.stubs(:affected_table_names).with(query).returns([@table2.name]).once
affected = @layer.send(:affected_tables)
affected.count.should eq 1
affected.should include @table2
end
describe '#affected_table_names' do
it 'should return the affected tables' do
sql = "select coalesce('tabname', null) from cdb_tablemetadata;select 1;select * from spatial_ref_sys"
@layer.send(:affected_table_names, sql).should =~ ["cartodb.cdb_tablemetadata", "public.spatial_ref_sys"]
end
end
end
describe '#backup' do
before(:all) do
Carto::VisualizationBackup.all.map(&:destroy)
@user = create_user
@map, @table1, @table_visualization, @visualization = create_full_visualization(@user)
end
after(:all) do
@map.destroy
@table1.destroy
@table_visualization.destroy
@visualization.destroy
@user.destroy
Carto::VisualizationBackup.all.map(&:destroy)
end
it 'creates a backup when layer is destroyed' do
layer = @visualization.layers.first
layer.destroy
Carto::VisualizationBackup.all.count.should eq 1
backup = Carto::VisualizationBackup.where(visualization_id: @visualization.id).first
backup.should_not eq nil
backup.user_id.should eq @user.id
backup.created_at.should_not eq nil
backup.category.should eq Carto::VisualizationBackup::CATEGORY_LAYER
backup.export.should_not be_empty
backup.destroy
end
end
end
@@ -0,0 +1,181 @@
require_relative '../../../spec_helper'
require_relative '../../../lib/fake_net_ldap_bind_as'
require 'fake_net_ldap'
describe Carto::Ldap::Configuration do
include_context 'organization with users helper'
before(:all) do
@domain_bases = [ "dc=cartodb" ]
@ldap_admin_username = 'user'
@ldap_admin_cn = "cn=#{@ldap_admin_username},#{@domain_bases[0]}"
@ldap_admin_password = '666'
@user_id_field = 'cn'
end
before(:each) do
FakeNetLdap.register_user(:username => @ldap_admin_cn, :password => @ldap_admin_password)
end
after(:each) do
FakeNetLdap.clear_user_registrations
FakeNetLdap.clear_query_registrations
end
it 'returns right away if empty user' do
ldap = Carto::Ldap::Configuration.new
ldap.authenticate('', 'something').should eq false
end
it 'returns right away if empty password' do
ldap = Carto::Ldap::Configuration.new
ldap.authenticate('some@one.es', '').should eq false
end
it 'tests basic authentication' do
auth_username = 'admin'
auth_cn = "cn=#{auth_username},#{@domain_bases[0]}"
auth_password = '000123456'
other_user_username = 'nobody2'
other_user_cn = "cn=#{other_user_username},#{@domain_bases[0]}"
other_user_password = 'nobodypass'
ldap_configuration = Carto::Ldap::Configuration.create({
organization_id: @organization.id,
host: "0.0.0.0",
port: 389,
domain_bases_list: @domain_bases,
connection_user: @ldap_admin_cn,
connection_password: @ldap_admin_password,
email_field: '.',
user_object_class: '.',
group_object_class: '.',
user_id_field: @user_id_field,
username_field: @user_id_field
})
# This uses ldap_admin credentials
ldap_configuration.test_connection[:success].should eq true
ldap_configuration.connection_user = 'wrong1'
ldap_configuration.test_connection[:success].should eq false
ldap_configuration.reload
ldap_configuration.connection_user = "cn=wrong2,#{@domain_bases[0]}"
ldap_configuration.test_connection[:success].should eq false
ldap_configuration.reload
ldap_configuration.connection_password = 'wrong'
ldap_configuration.test_connection[:success].should eq false
register_ldap_user(auth_cn, auth_username, auth_password)
result = ldap_configuration.authenticate(auth_username, auth_password)
result.should_not eq false
result.class.should eq Carto::Ldap::Entry
result.user_id.should eq auth_username
# Add other user
register_ldap_user(other_user_cn, other_user_username, other_user_password)
result = ldap_configuration.authenticate(auth_username, auth_password)
result.should_not eq false
result.user_id.should eq auth_username
result = ldap_configuration.authenticate(other_user_username, other_user_password)
result.should_not eq false
result.user_id.should eq other_user_username
# Test connection error
Net::LDAP.any_instance.stubs(:bind_as).raises(Net::LDAP::Error.new)
result = ldap_configuration.authenticate(auth_username, auth_password)
result.should be_false
ldap_configuration.delete
end
it 'Checks user search' do
user_a_username = 'user-A'
user_a_cn = "cn=#{user_a_username},#{@domain_bases[0]}"
user_a_password = '000123456'
user_b_username = 'user-B'
user_b_cn = "cn=#{user_b_username},#{@domain_bases[0]}"
user_b_password = '789012'
ldap_configuration = Carto::Ldap::Configuration.create({
organization_id: @organization.id,
host: "0.0.0.0",
port: 389,
domain_bases_list: @domain_bases,
connection_user: @ldap_admin_cn,
connection_password: @ldap_admin_password,
email_field: '.',
user_object_class: 'organizationalRole',
group_object_class: '.',
user_id_field: @user_id_field,
username_field: @user_id_field
})
register_ldap_user(user_a_cn, user_a_username, user_a_password)
register_ldap_user(user_b_cn, user_b_username, user_b_password)
ldap_search_user_entries = [
{ @user_id_field => [user_a_username] },
{ @user_id_field => [user_b_username] }
]
FakeNetLdap.register_query(Net::LDAP::Filter.eq('objectClass', ldap_configuration.user_object_class),
ldap_search_user_entries)
ldap_configuration.users.should_not eq false
ldap_configuration.users.count.should eq 2
search_results = ldap_configuration.users.map { |user_data|
user_data['cn'].first
}
search_results.should eq [ user_a_username, user_b_username ]
ldap_configuration.delete
end
it "Doens't allows to change user_id_field once set" do
# Dumb spec, but to make sure if this gets changed we notice
ldap_configuration = Carto::Ldap::Configuration.create({
organization_id: @organization.id,
host: "0.0.0.0",
port: 389,
domain_bases_list: @domain_bases,
connection_user: @ldap_admin_cn,
connection_password: @ldap_admin_password,
email_field: '.',
user_object_class: 'organizationalRole',
group_object_class: '.',
user_id_field: @user_id_field,
username_field: @user_id_field
})
ldap_configuration.user_id_field = "modified"
ldap_configuration.save
ldap_configuration.reload
ldap_configuration.user_id_field.should eq @user_id_field
ldap_configuration.delete
end
private
def register_ldap_user(cn, username, password)
ldap_entry_data = {
dn: cn,
@user_id_field => [username]
}
# Data to return as an LDAP result, that will be loaded into Carto::Ldap::Entry
FakeNetLdap.register_user(:username => cn, :password => password)
FakeNetLdap.register_query(Net::LDAP::Filter.eq('cn', username), [ldap_entry_data])
end
end
+181
View File
@@ -0,0 +1,181 @@
require 'spec_helper_min'
require 'factories/carto_visualizations'
module Carto
describe Legend do
include Carto::Factories::Visualizations
before(:all) do
@user = FactoryGirl.create(:carto_user)
@map, @table, @table_visualization, @visualization = create_full_visualization(@user)
@layer = @visualization.layers.find(&:data_layer?)
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
@user.destroy
end
it 'notifies layer change on commit' do
@layer.visualization.send(:invalidation_service).expects(:invalidate).times(3)
legend = Legend.create!(layer: @layer,
type: 'bubble',
definition: { color: '#abc' })
legend.update_attributes!(title: 'Cool legend')
legend.destroy
end
describe '#validations' do
before(:all) do
@legend = Legend.new(layer_id: @layer.id)
@legend.valid?
end
after(:all) { @legend = nil }
it 'won\'t crash validation if layer is nil' do
legend = Legend.new
expect { legend.save }.to_not raise_error
legend.errors.should_not be_empty
end
it 'requires a type' do
@legend.errors[:type].should_not be_empty
end
it 'only accepts known types' do
Legend::VALID_LEGEND_TYPES.each do |type|
@legend.type = type
@legend.valid?
@legend.errors[:type].should be_empty
end
@legend.type = 'foo'
@legend.valid?
@legend.errors[:type].should_not be_empty
end
it 'requires a layer' do
legend = Carto::Legend.new
legend.stubs(:on_data_layer)
legend.stubs(:under_max_legends_per_layer)
legend.valid?
legend.errors[:layer].should_not be_empty
end
it 'only accepts data layers' do
legend = Legend.new(layer_id: @visualization.layers.first.id)
legend.valid?
expected_error = '\'tiled\' layers can\'t have legends'
legend.errors[:layer].should include(expected_error)
end
it 'accepts torque layers' do
Carto::Layer.any_instance.stubs(:kind)
.returns('torque')
legend = Legend.new(layer_id: @layer.id)
legend.valid?
legend.errors[:layer].should be_empty
end
it 'doesn\'t require a title' do
@legend.errors[:title].should be_empty
end
it 'doesn\'t require pre_html' do
@legend.errors[:pre_html].should be_empty
end
it 'doesn\'t require post_html' do
@legend.errors[:pre_html].should be_empty
end
it 'requires a definition' do
@legend.errors[:definition].should_not be_empty
@legend.errors[:definition].should include('could not be validated')
end
it 'validates definition' do
legend = Legend.new(layer_id: @layer.id, type: 'bubble', definition: {})
legend.valid?
legend.errors[:definition].should_not be_empty
legend.errors[:definition].should_not include('could not be validated')
end
it 'rejects empty definitions for custom' do
legend = Legend.new(layer_id: @layer.id, type: 'custom', definition: {})
legend.valid?.should be_false
legend.errors[:definition].should_not be_empty
end
describe('#conf') do
it 'rejects spam' do
bad_conf = { not_column_name: 'not array' }
legend = Legend.new(layer_id: @layer.id,
type: 'bubble',
definition: {},
conf: bad_conf)
legend.valid?
legend.errors[:conf].should_not be_empty
legend.errors[:conf].should_not include('could not be validated')
end
it 'rejects incorrect type for \'columns\'' do
bad_conf = { columns: 'not array' }
legend = Legend.new(layer_id: @layer.id,
type: 'bubble',
definition: {},
conf: bad_conf)
legend.valid?
legend.errors[:conf].should_not be_empty
legend.errors[:conf].should_not include('could not be validated')
end
it 'rejects incorrect type for \'columns\' contents' do
bad_conf = { columns: ['manolo', 'escobar', 3] }
legend = Legend.new(layer_id: @layer.id,
type: 'bubble',
definition: {},
conf: bad_conf)
legend.valid?
legend.errors[:conf].should_not be_empty
legend.errors[:conf].should_not include('could not be validated')
end
it 'accepts proper conf' do
bad_conf = { columns: ['manolo', 'escobar'] }
legend = Legend.new(layer_id: @layer.id,
type: 'bubble',
definition: {},
conf: bad_conf)
legend.valid?
legend.errors[:conf].should be_empty
end
end
it 'rejects html type' do
legend = Legend.new(layer_id: @layer.id, type: 'html', definition: {})
legend.valid?.should be_false
legend.errors[:type].should_not be_empty
end
end
end
end
+109
View File
@@ -0,0 +1,109 @@
require_relative '../../spec_helper'
describe Carto::Map do
include Carto::Factories::Visualizations
before(:all) do
@user = create_user
@carto_user = Carto::User.find(@user.id)
end
before(:each) do
bypass_named_maps
delete_user_data(@user)
end
after(:all) do
bypass_named_maps
@user.destroy
end
it "Tests layer ordering" do
table = ::Table.new
table.user_id = @user.id
table.save
map = ::Map.create(user_id: @user.id, table_id: table.id)
5.times do
map.add_layer(::Layer.create(kind: 'carto'))
end
3.times do
map.add_layer(Layer.create(kind: 'tiled'))
end
map.add_layer(Layer.create(kind: 'torque'))
layers_count = map.layers.count
layers_count.should eq 9
base_layers_count = map.base_layers.count
base_layers_count.should eq 9
carto_layers_count = map.carto_layers.count
carto_layers_count.should eq 5
user_layers_count = map.user_layers.count
user_layers_count.should eq 3
carto_and_torque_layers_count = map.data_layers.count
carto_and_torque_layers_count.should eq 6
torque_layers_count = map.torque_layers.count
torque_layers_count.should eq 1
other_layers_count = map.other_layers.count
other_layers_count.should eq 1 # torque one counts as "other"
map_new = Carto::Map.where(id: map.id).first
layers_count.should eq map_new.layers.count
base_layers_count.should eq map_new.base_layers.count
carto_layers_count.should eq map_new.carto_layers.count
user_layers_count.should eq map_new.user_layers.count
carto_and_torque_layers_count.should eq map_new.data_layers.count
torque_layers_count.should eq map_new.torque_layers.count
other_layers_count.should eq map_new.other_layers.count
map.layers.map(&:id).should eq map_new.layers.map(&:id)
map.base_layers.map(&:id).should eq map_new.base_layers.map(&:id)
map.data_layers.map(&:id).should eq map_new.data_layers.map(&:id)
map.user_layers.map(&:id).should eq map_new.user_layers.map(&:id)
map.data_layers.map(&:id).should eq map_new.data_layers.map(&:id)
map.torque_layers.map(&:id).should eq map_new.torque_layers.map(&:id)
map.other_layers.map(&:id).should eq map_new.other_layers.map(&:id)
map.destroy
end
describe '#update_dataset_dependencies' do
before(:all) do
@carto_layer = FactoryGirl.create(:carto_layer, kind: 'carto')
@torque_layer = FactoryGirl.create(:carto_layer, kind: 'torque')
@map = Carto::Map.create(user: @carto_user, layers: [@carto_layer, @torque_layer])
end
after(:all) do
@torque_layer.destroy
@carto_layer.destroy
@map.destroy
end
it 'updates dependencies of carto layers' do
@map.layers.select(&:carto?).first.expects(:register_table_dependencies).once
@map.update_dataset_dependencies
end
it 'updates dependencies of carto layers' do
@map.layers.select(&:torque?).first.expects(:register_table_dependencies).once
@map.update_dataset_dependencies
end
end
it '#save should trigger invalidation' do
@map, @table, @table_visualization, @visualization = create_full_visualization(@carto_user)
@visualization.send(:invalidation_service).expects(:invalidate)
@map.save
end
end
+264
View File
@@ -0,0 +1,264 @@
require 'spec_helper_min'
require 'support/helpers'
describe Carto::Mapcap do
include Carto::Factories::Visualizations
before(:all) do
@user = FactoryGirl.create(:carto_user, private_tables_enabled: true)
@map, @table, @table_visualization, @visualization = create_full_visualization(@user)
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
@user.destroy
end
describe '#ids_vizjson' do
before(:all) do
@mapcap = Carto::Mapcap.create!(visualization_id: @visualization.id)
@ids_json = @mapcap.ids_json
end
after(:all) do
@mapcap.destroy
@ids_json = nil
end
it 'should have visualization_id' do
@ids_json[:visualization_id].should_not be_nil
end
it 'should have map_id' do
@ids_json[:map_id].should_not be_nil
end
it 'should have correct visualization_id' do
@ids_json[:visualization_id].should eq @visualization.id
end
it 'should have correct map_id' do
@ids_json[:map_id].should eq @map.id
end
describe 'with layers' do
before(:all) do
@carto_layer = FactoryGirl.create(:carto_layer, kind: 'carto', maps: [@map])
@visualization.reload
@mapcap = Carto::Mapcap.create!(visualization_id: @visualization.id)
@ids_json_layers = @mapcap.ids_json[:layers]
end
after(:all) do
@mapcap.destroy
@carto_layer.destroy
@visualization.reload
@ids_json_layers = nil
end
it 'should not have empty layers' do
@ids_json_layers.should_not be_empty
end
it 'should contain layer ids and in the right order' do
@ids_json_layers.count.should eq @visualization.layers.count
@ids_json_layers.each_with_index do |layer, index|
layer[:layer_id].should eq @visualization.layers[index].id
end
end
describe 'with widgets' do
before(:all) do
@widget = FactoryGirl.create(:widget, layer: @carto_layer)
@visualization.reload
@mapcap = Carto::Mapcap.create!(visualization_id: @visualization.id)
@ids_json_layers = @mapcap.ids_json[:layers]
end
after(:all) do
@widget.destroy
@visualization.reload
@mapcap.destroy
@ids_json_layers = nil
end
it 'should contain widgets only for layers with widgets and in the right order' do
@visualization.layers.each_with_index do |layer, index|
@ids_json_layers[index][:widgets].each_with_index do |widget_id, widget_index|
widget_id.should eq layer.widgets[widget_index].id
end
end
end
end
end
end
describe '#regenerate_visualization' do
before(:all) do
FactoryGirl.create(:analysis, visualization: @visualization, user: @user)
FactoryGirl.create(:widget, layer: @visualization.data_layers.first)
@visualization.reload
@mapcap = Carto::Mapcap.create!(visualization_id: @visualization.id)
end
after(:all) do
@mapcap.destroy
@ids_json = nil
end
it 'tokens should be functional from regenerated visualizations after privacy changes' do
@visualization.create_mapcap!
@visualization.privacy = Carto::Visualization::PRIVACY_PROTECTED
@visualization.password = "r4nr0mp455"
@visualization.save!
mapcap = @visualization.create_mapcap!
mapcap.regenerate_visualization.non_mapcapped.auth_token
end
it 'should preserve map' do
regenerated_visualization = @mapcap.regenerate_visualization
regenerated_visualization.map.id.should eq @map.id
end
it 'should preserve visualization' do
regenerated_visualization = @mapcap.regenerate_visualization
regenerated_visualization.id.should eq @visualization.id
end
it 'should preserve user' do
regenerated_visualization = @mapcap.regenerate_visualization
regenerated_visualization.user.id.should eq @visualization.user.id
end
it 'should preserve permission' do
regenerated_visualization = @mapcap.regenerate_visualization
regenerated_visualization.permission.id.should eq @visualization.permission.id
end
it 'should preserve analyses' do
regenerated_visualization = @mapcap.regenerate_visualization
analysis = @visualization.analyses.first
regenerated_analysis = regenerated_visualization.analyses.first
expect(regenerated_analysis.analysis_definition).to eq analysis.analysis_definition
end
it 'should preserve widgets' do
regenerated_visualization = @mapcap.regenerate_visualization
widget = @visualization.widgets.first
regenerated_widget = regenerated_visualization.widgets.first
expect(regenerated_widget.order).to eq widget.order
expect(regenerated_widget.type).to eq widget.type
expect(regenerated_widget.title).to eq widget.title
expect(regenerated_widget.options).to eq widget.options
expect(regenerated_widget.source_id).to eq widget.source_id
expect(regenerated_widget.style).to eq widget.style
end
it 'should be readonly' do
rv = @mapcap.regenerate_visualization
rv.readonly?.should eq true
rv.user.readonly?.should eq true
rv.full_user.readonly?.should eq true
rv.permission.readonly?.should eq true
rv.likes.each { |like| like.readonly?.should eq true }
rv.shared_entities.each { |entity| entity.readonly?.should eq true }
rv.unordered_children.each { |child| child.readonly?.should eq true }
rv.overlays.each { |overlay| overlay.readonly?.should eq true }
rv.active_layer.readonly?.should eq true
rv.map.readonly?.should eq true
rv.related_templates.each { |template| template.readonly?.should eq true }
rv.external_sources.each { |resource| resource.readonly?.should eq true }
rv.analyses.each { |analysis| analysis.readonly?.should eq true }
rv.mapcaps.each { |mapcap| mapcap.readonly?.should eq true }
rv.state.readonly?.should eq true
rv.snapshots.each { |snapshot| snapshot.readonly?.should eq true }
end
describe 'without user DB' do
before(:all) do
@user_nodb = FactoryGirl.create(:carto_user, private_tables_enabled: true)
@map_nodb, @table_nodb, @table_visualization_nodb, @visualization_nodb = create_full_visualization(@user_nodb)
@mapcap_nodb = Carto::Mapcap.create!(visualization_id: @visualization_nodb.id)
@actual_db_name = @user_nodb.database_name
@user_nodb.update_attribute(:database_name, 'wadus')
@mapcap_nodb.reload
end
after(:all) do
@user_nodb.update_attribute(:database_name, @actual_db_name)
destroy_full_visualization(@map_nodb, @table_nodb, @table_visualization_nodb, @visualization_nodb)
@user_nodb.destroy
end
it 'should work' do
CartoDB::Logger.expects(:warning).never
User.any_instance.expects(:in_database).never
@mapcap_nodb.regenerate_visualization
User.any_instance.unstub(:in_database)
end
end
describe 'with layers' do
before(:all) do
@carto_layer = FactoryGirl.create(:carto_layer, kind: 'carto', maps: [@map])
@visualization.reload
@mapcap = Carto::Mapcap.create!(visualization_id: @visualization.id)
end
after(:all) do
@mapcap.destroy
@carto_layer.destroy
@visualization.reload
end
it 'should contain same layers in same order' do
CartoDB::Logger.expects(:warning).never
User.any_instance.stubs(:in_database).raises("Mapcap regeneration shouldn't touch user database")
Carto::User.any_instance.stubs(:in_database).raises("Mapcap regeneration shouldn't touch user database")
regenerated_visualization = @mapcap.regenerate_visualization
regenerated_visualization.layers.each_with_index do |layer, index|
expect(layer.id).to eq(regenerated_visualization.layers[index].id)
end
Carto::User.any_instance.unstub(:in_database)
User.any_instance.unstub(:in_database)
end
describe 'with widgets' do
before(:all) do
@widget = FactoryGirl.create(:widget, layer: @carto_layer)
@visualization.reload
@mapcap = Carto::Mapcap.create!(visualization_id: @visualization.id)
end
after(:all) do
@widget.destroy
@visualization.reload
@mapcap.destroy
end
it 'should contain widgets only for layers with widgets and in the right order' do
@visualization.layers.each_with_index do |layer, index|
regenerated_visualization = @mapcap.regenerate_visualization
regenerated_layer = regenerated_visualization.layers[index]
layer.widgets.length.should eq regenerated_layer.widgets.length
layer.widgets.each_with_index do |widget, widget_index|
widget.should eq regenerated_layer.widgets[widget_index]
end
end
end
end
end
end
end
@@ -0,0 +1,36 @@
require_relative '../../spec_helper_min'
describe Carto::Api::MobileAppPresenter do
it "Compares old and new ways of 'presenting' mobile app data" do
user = FactoryGirl.build(:user, :mobile)
mobile_app = FactoryGirl.build(:mobile_app)
# Data should return mobile platforms data
compare_data(mobile_app.data(user, fetch_mobile_platforms: true), mobile_app.as_json, fetch_mobile_platforms: true)
# Data should return mobile app types data
compare_data(mobile_app.data(user, fetch_app_types: true), mobile_app.as_json, fetch_app_types: true)
# Data should return default mobile app data
compare_data(mobile_app.data(user), mobile_app.as_json)
end
protected
def compare_data(old_data, new_data, fetch_mobile_platforms: false, fetch_app_types: false)
new_data['id'].should == old_data[:id]
new_data['name'].should == old_data[:name]
new_data['description'].should == old_data[:description]
new_data['icon_url'].should == old_data[:icon_url]
new_data['platform'].should == old_data[:platform]
new_data['app_id'].should == old_data[:app_id]
new_data['app_type'].should == old_data[:app_type]
new_data['license_key'].should == old_data[:license_key]
new_data['monthly_users'].should == old_data[:monthly_users]
expect(old_data[:mobile_platforms]).to be_nil if fetch_mobile_platforms == false
expect(old_data[:app_types]).to be_nil if fetch_app_types == false
expect(old_data[:mobile_platforms]).not_to be_empty if fetch_mobile_platforms == true
expect(old_data[:app_types]).not_to be_empty if fetch_app_types == true
end
end
+168
View File
@@ -0,0 +1,168 @@
require 'spec_helper_min'
module Carto
describe Notification do
before(:all) do
@sequel_organization = FactoryGirl.create(:organization_with_users)
@organization = Carto::Organization.find(@sequel_organization.id)
@organization.users[1].update_attribute(:viewer, true)
end
after(:all) do
@sequel_organization.destroy
end
describe '#validation' do
it 'passes for valid notification' do
n = Notification.new(icon: Carto::Notification::ICON_ALERT, body: 'Hello, friend!')
expect(n).to be_valid
end
it 'passes for valid organization notification' do
n = Notification.new(icon: Carto::Notification::ICON_ALERT,
body: 'Hello, friend!',
organization: @organization,
recipients: 'builders')
expect(n).to be_valid
end
describe 'icon' do
it 'is required' do
n = Notification.new
expect(n).not_to be_valid
expect(n.errors).to include :icon
end
it 'cannot be blank' do
n = Notification.new(icon: '')
expect(n).not_to be_valid
expect(n.errors).to include :icon
end
it 'should exist' do
n = Notification.new(icon: 'wadus')
expect(n).not_to be_valid
expect(n.errors).to include :icon
end
end
describe 'recipients' do
it 'is required if organization is present' do
n = Notification.new(organization: @organization)
expect(n).not_to be_valid
expect(n.errors).to include :recipients
end
it 'is not required if organization is not present' do
n = Notification.new
n.valid?
expect(n.errors).not_to include :recipients
end
it 'does not accept arbitrary values' do
n = Notification.new(recipients: 'everybody')
expect(n).not_to be_valid
expect(n.errors).to include :recipients
end
end
describe 'body' do
it 'is required' do
n = Notification.new
expect(n).not_to be_valid
expect(n.errors).to include :body
end
it 'cannot be empty' do
n = Notification.new(body: '')
expect(n).not_to be_valid
expect(n.errors).to include :body
end
it 'cannot be longer than 140 characters' do
n = Notification.new(body: 'a' * 141)
expect(n).not_to be_valid
expect(n.errors).to include :body
end
it 'cannot contain Markdown blocks' do
n = Notification.new(body: '![this is](an image])')
expect(n).not_to be_valid
expect(n.errors).to include :body
n = Notification.new(body: '> a block quote')
expect(n).not_to be_valid
expect(n.errors).to include :body
n = Notification.new(body: '# a header')
expect(n).not_to be_valid
expect(n.errors).to include :body
end
it 'does not count Markdown characters towards length' do
# Should only count `linkb` (and 135 a's). Ignores the link and `_*`
n = Notification.new(body: '[link](should not be counted)_*b*_' + 'a' * 135)
n.valid?
expect(n.errors).not_to include :body
end
end
end
it 'should be deleted when the organization is destroyed' do
org = FactoryGirl.create(:organization)
n = Notification.create!(organization_id: org.id,
icon: Carto::Notification::ICON_ALERT,
recipients: 'all',
body: 'Hey!')
org.destroy
expect(Notification.exists?(n.id)).to be_false
end
describe '#after_create' do
it 'for non-org notifications should not send the notification to users' do
n = Notification.create(icon: Carto::Notification::ICON_ALERT, body: 'Hello, friend!')
expect(n.received_notifications).to be_empty
end
describe 'for org notifications' do
before(:each) do
@notification = Notification.new(icon: Carto::Notification::ICON_ALERT,
body: 'Hello, friend!',
organization: @organization)
end
after(:each) do
@notification.destroy
end
it 'should send the notification to all current organization members' do
@notification.recipients = 'all'
@notification.save!
expect(@notification.received_notifications.map(&:user_id)).to eq @organization.users.map(&:id)
end
it 'sent notifications should be unread' do
@notification.recipients = 'all'
@notification.save!
expect(@notification.received_notifications.map(&:read_at).none?).to be_true
end
it 'should send the notification to current organization builders' do
@notification.recipients = 'builders'
@notification.save!
expect(@notification.received_notifications.map(&:user_id)).to eq @organization.builder_users.map(&:id)
end
it 'should send the notification to current organization viewers' do
@notification.recipients = 'viewers'
@notification.save!
expect(@notification.received_notifications.map(&:user_id)).to eq @organization.viewer_users.map(&:id)
end
end
end
end
end
@@ -0,0 +1,609 @@
require 'spec_helper_min'
require 'helpers/database_connection_helper'
module Carto
describe OauthAccessToken do
include_context 'organization with users helper'
include DatabaseConnectionHelper
describe '#validation' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.create!(user: @carto_user, oauth_app: @app)
@user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @carto_user.id)
end
after(:all) do
@user.destroy
@app.destroy
@user_table.destroy
end
it 'does not accept invalid scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ['wadus'])
expect(access_token).to_not(be_valid)
expect(access_token.errors[:scopes]).to(include("contains unsupported scopes: wadus"))
end
it 'does accept create tables in schema scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ["schemas:c"])
expect(access_token).to(be_valid)
end
it 'does accept read datasets scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ["datasets:r:#{@user_table.name}"])
expect(access_token).to(be_valid)
end
it 'does accept read and write datasets scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ["datasets:rw:#{@user_table.name}"])
expect(access_token).to(be_valid)
end
it 'does not accept invalid permission in datasets scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ['datasets:f:wadus'])
expect(access_token).to_not(be_valid)
end
it 'does not validate tables existence on datasets scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ['datasets:r:wadus'])
expect(access_token).to(be_valid)
end
it 'does not validate schemas existence on datasets scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ['schemas:c:wadus'])
expect(access_token).to(be_valid)
end
it 'does not accept invalid datasets scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ['datasets:f'])
expect(access_token).to_not(be_valid)
end
it 'does not accept invalid schema scopes' do
access_token = OauthAccessToken.new(oauth_app_user: @app_user, scopes: ['schemas:f'])
expect(access_token).to_not(be_valid)
end
it 'auto generates api_key' do
access_token = OauthAccessToken.create!(oauth_app_user: @app_user)
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
end
it 'api key includes permissions for requested scopes' do
access_token = OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ['dataservices:geocoding', 'user:profile'])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(include(type: 'apis', apis: ['sql']))
expect(access_token.api_key.grants).to(include(type: 'dataservices', services: ['geocoding']))
expect(access_token.api_key.grants).to(include(type: 'user', data: ['profile']))
end
it 'raises an error when creating an api key for an non-existent dataset' do
expect {
OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ['datasets:r:wadus'])
}.to raise_error(ActiveRecord::RecordInvalid)
end
it 'raises an error when creating an api key for an non-existent dataset' do
expect {
OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ['schemas:c:wadus'])
}.to raise_error(ActiveRecord::RecordInvalid)
end
it 'includes create permission for schemas scopes' do
FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @user.id)
expected_grants =
[
{
type: 'apis',
apis: ['sql']
},
{
type: 'database',
schemas: [
{
name: 'public',
permissions: ['create']
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ['schemas:c'])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'includes listing metadata permission for datasets scopes' do
FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @user.id)
expected_grants =
[
{
type: 'apis',
apis: ['sql']
},
{
type: 'database',
table_metadata: []
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user, scopes: ['datasets:metadata'])
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'api key includes read permissions for datasets scopes' do
user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @user.id)
expected_grants =
[
{
type: 'apis',
apis: ['maps', 'sql']
},
{
type: 'database',
tables: [
{
name: user_table.name,
permissions: ['select'],
schema: 'public'
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ["datasets:r:#{user_table.name}"])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'api key includes read-write permissions for datasets scopes' do
user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @user.id)
expected_grants =
[
{
type: 'apis',
apis: ['maps', 'sql']
},
{
type: 'database',
tables: [
{
name: user_table.name,
permissions: ['select', 'insert', 'update', 'delete'],
schema: 'public'
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ["datasets:rw:#{user_table.name}"])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'api key includes permissions for several datasets scopes' do
user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @user.id)
user_table2 = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @user.id)
expected_grants =
[
{
type: 'apis',
apis: ['maps', 'sql']
},
{
type: 'database',
tables: [
{
name: user_table.name,
permissions: ['select'],
schema: 'public'
},
{
name: user_table2.name,
permissions: ['select', 'insert', 'update', 'delete'],
schema: 'public'
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: [
"datasets:r:#{user_table.name}",
"datasets:rw:#{user_table2.name}"
])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'includes service account permission for user scopes' do
FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @user.id)
expected_grants =
[
{
type: 'apis',
apis: ['do']
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user, scopes: ['apis:do'])
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
end
describe 'cdb_conf_info' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.create!(user: @carto_user, oauth_app: @app)
@user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @carto_user.id)
@db_role = Carto::DB::Sanitize.sanitize_identifier("carto_role_#{SecureRandom.hex}")
Carto::ApiKey.any_instance.stubs(:db_role).returns(@db_role)
end
after(:all) do
@user.destroy
@app.destroy
@user_table.destroy
Carto::ApiKey.any_instance.unstub(:db_role)
end
it 'saves ownership_role_name in cdb_conf_info if schemas granted' do
Carto::ApiKey.any_instance.expects(:cdb_conf_info)
.returns(username: @app_user.user.username,
permissions: [],
ownership_role_name: @app_user.ownership_role_name)
.at_least_once
OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: [
"schemas:c"
])
end
it 'does not save ownership_role_name in cdb_conf_info if schemas not granted' do
Carto::ApiKey.any_instance.expects(:cdb_conf_info)
.returns(username: @app_user.user.username,
permissions: [],
ownership_role_name: '')
.at_least_once
OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: [
"datasets:r:#{@user_table.name}"
])
end
end
describe '#shared datasets' do
before :each do
@app = FactoryGirl.create(:oauth_app, user: @carto_org_user_1)
@app_user = OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app)
@shared_table = create_table(user_id: @carto_org_user_1.id)
@not_shared_table = create_table(user_id: @carto_org_user_1.id)
perm = @shared_table.table_visualization.permission
perm.acl = [{ type: 'user', entity: { id: @carto_org_user_2.id }, access: 'rw' }]
perm.save!
@shared_dataset_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@shared_table.name}"
@non_shared_dataset_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@not_shared_table.name}"
end
after :each do
@app_user.destroy
@app.destroy
end
it 'works with shared dataset' do
expected_grants =
[
{
type: 'apis',
apis: ['maps', 'sql']
},
{
type: 'database',
tables: [
{
name: @shared_table.name,
permissions: ['select', 'insert', 'update', 'delete'],
schema: @carto_org_user_1.database_schema
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user, scopes: [@shared_dataset_scope])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("select count(1) from #{@carto_org_user_1.database_schema}.#{@shared_table.name}")
end
end
it 'should fail with non shared dataset' do
expect {
OauthAccessToken.create!(oauth_app_user: @app_user, scopes: [@non_shared_dataset_scope])
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant table permissions you have/)
end
it 'should fail with non shared schema' do
expect {
OauthAccessToken.create!(oauth_app_user: @app_user, scopes: ['schemas:c:non_existent'])
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant schema permissions you have/)
end
it 'should fail with shared and non shared dataset' do
expect {
OauthAccessToken.create!(
oauth_app_user: @app_user,
scopes: [@shared_dataset_scope, @non_shared_dataset_scope]
)
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant table permissions you have/)
end
describe 'read - write permissions' do
before :each do
@only_read_table = create_table(user_id: @carto_org_user_1.id)
perm = @only_read_table.table_visualization.permission
perm.acl = [{ type: 'user', entity: { id: @carto_org_user_2.id }, access: 'r' }]
perm.save!
end
after :each do
@only_read_table.destroy
end
it 'should fail write scope in shared dataset with only read perms' do
rw_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@only_read_table.name}"
expect {
OauthAccessToken.create!(oauth_app_user: @app_user, scopes: [rw_scope])
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant table permissions you have/)
end
end
end
describe 'organization shared datasets' do
before :each do
@app = FactoryGirl.create(:oauth_app, user: @carto_org_user_1)
@app_user = OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app)
@org_shared_table = create_table(user_id: @carto_org_user_1.id)
@non_org_shared_table = create_table(user_id: @carto_org_user_1.id)
perm = @org_shared_table.table_visualization.permission
perm.acl = [
{
type: Permission::TYPE_ORGANIZATION,
entity: { id: @carto_organization.id },
access: Permission::ACCESS_READWRITE
}
]
perm.save!
@org_shared_dataset_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@org_shared_table.name}"
@non_org_shared_dataset_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@non_org_shared_table.name}"
end
after :each do
@app_user.destroy
@app.destroy
end
it 'works with shared dataset' do
expected_grants =
[
{
type: 'apis',
apis: ['maps', 'sql']
},
{
type: 'database',
tables: [
{
name: @org_shared_table.name,
permissions: ['select', 'insert', 'update', 'delete'],
schema: @carto_org_user_1.database_schema
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user, scopes: [@org_shared_dataset_scope])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'works with shared schema' do
expected_grants =
[
{
type: 'apis',
apis: ['sql']
},
{
type: 'database',
schemas: [
{
name: @carto_org_user_2.database_schema,
permissions: ['create']
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ["schemas:c:#{@carto_org_user_2.database_schema}"])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'should fail with non shared dataset' do
expect {
OauthAccessToken.create!(oauth_app_user: @app_user, scopes: [@non_org_shared_dataset_scope])
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant table permissions you have/)
end
it 'should fail with non shared schema' do
expect {
OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ["schemas:c:wadus"])
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant schema permissions you have/)
end
it 'should fail with shared and non shared dataset' do
expect {
OauthAccessToken.create!(
oauth_app_user: @app_user,
scopes: [@org_shared_dataset_scope, @non_org_shared_dataset_scope]
)
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant table permissions you have/)
end
describe 'read - write permissions' do
before :each do
@only_read_table = create_table(user_id: @carto_org_user_1.id)
perm = @only_read_table.table_visualization.permission
perm.acl = [
{
type: Permission::TYPE_ORGANIZATION,
entity: { id: @carto_organization.id },
access: Permission::ACCESS_READONLY
}
]
perm.save!
end
after :each do
@only_read_table.destroy
end
it 'should fail write scope in shared dataset with only read perms' do
rw_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@only_read_table.name}"
expect {
OauthAccessToken.create!(oauth_app_user: @app_user, scopes: [rw_scope])
}.to raise_error(ActiveRecord::RecordInvalid, /can only grant table permissions you have/)
end
end
end
describe 'views' do
before :all do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@user_table = create_table(user_id: @carto_user.id)
@view_name = "#{@user_table.name}_view"
@materialized_view_name = "#{@user_table.name}_matview"
@carto_user.in_database do |db|
query = %{
CREATE VIEW #{@view_name} AS SELECT * FROM #{@user_table.name};
CREATE MATERIALIZED VIEW #{@materialized_view_name} AS SELECT * FROM #{@user_table.name};
}
db.execute(query)
end
end
before :each do
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.create!(user: @carto_user, oauth_app: @app)
end
after :each do
@app_user.destroy
@app.destroy
end
after :all do
@carto_user.in_database do |db|
query = %{
DROP VIEW #{@view_name};
DROP MATERIALIZED VIEW #{@materialized_view_name};
}
db.execute(query)
end
@user_table.destroy
@user.destroy
@carto_user.destroy
end
it 'validates view scope' do
expected_grants =
[
{
type: 'apis',
apis: ['maps', 'sql']
},
{
type: 'database',
tables: [
{
name: @view_name,
permissions: ['select', 'insert', 'update', 'delete'],
schema: @carto_user.database_schema
}
]
}
]
access_token = OauthAccessToken.create!(oauth_app_user: @app_user, scopes: ["datasets:rw:#{@view_name}"])
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
it 'validates materialized view scope' do
expected_grants =
[
{
type: 'apis',
apis: ['maps', 'sql']
},
{
type: 'database',
tables: [
{
name: @materialized_view_name,
permissions: ['select', 'insert', 'update', 'delete'],
schema: @carto_user.database_schema
}
]
}
]
access_token = OauthAccessToken.create!(
oauth_app_user: @app_user,
scopes: ["datasets:rw:#{@materialized_view_name}"]
)
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.api_key.grants).to(eq(expected_grants))
end
end
end
end
@@ -0,0 +1,60 @@
require 'spec_helper_min'
module Carto
describe OauthAppOrganization do
describe '#validation' do
before(:all) do
@user = FactoryGirl.create(:carto_user)
@organization = FactoryGirl.create(:carto_organization)
@app = FactoryGirl.create(:oauth_app, user: @user)
end
after(:all) do
@user.destroy
@app.destroy
end
it 'requires organization' do
app_organization = OauthAppOrganization.new
expect(app_organization).to_not(be_valid)
expect(app_organization.errors[:organization]).to(include("can't be blank"))
end
it 'requires oauth app' do
app_organization = OauthAppOrganization.new
expect(app_organization).to_not(be_valid)
expect(app_organization.errors[:oauth_app]).to(include("can't be blank"))
end
it 'does not allow duplicates' do
begin
@app_organization1 = OauthAppOrganization.create!(organization: @organization, oauth_app: @app, seats: 1)
app_organization2 = OauthAppOrganization.new(organization: @organization, oauth_app: @app, seats: 1)
expect(app_organization2).to_not(be_valid)
expect(app_organization2.errors[:organization]).to(include("has already been taken"))
ensure
@app_organization1.destroy if @app_organization1
end
end
it 'requires positive seats' do
app_organization = OauthAppOrganization.new
expect(app_organization).to_not(be_valid)
expect(app_organization.errors[:seats]).to(include("can't be blank"))
app_organization.seats = 'wadus'
expect(app_organization).to_not(be_valid)
expect(app_organization.errors[:seats]).to(include("is not a number"))
app_organization.seats = 0
expect(app_organization).to_not(be_valid)
expect(app_organization.errors[:seats]).to(include("must be greater than 0"))
end
it 'validates' do
app_organization = OauthAppOrganization.new(organization: @organization, oauth_app: @app, seats: 5)
expect(app_organization).to(be_valid)
end
end
end
end
+350
View File
@@ -0,0 +1,350 @@
require 'spec_helper_min'
module Carto
describe OauthApp do
describe '#validation' do
before(:all) do
@user = FactoryGirl.create(:carto_user)
end
it 'requires user' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
app = OauthApp.new
expect(app).to_not(be_valid)
expect(app.errors[:user]).to(include("can't be blank"))
Cartodb::Central.unstub(:sync_data_with_cartodb_central?)
end
it 'requires name' do
app = OauthApp.new
expect(app).to_not(be_valid)
expect(app.errors[:name]).to(include("can't be blank"))
app.name = ''
expect(app).to_not(be_valid)
expect(app.errors[:name]).to(include("can't be blank"))
end
it 'rejects if icon_url invalid' do
app = OauthApp.new
app.icon_url = 'carto.com'
expect(app).to_not(be_valid)
expect(app.errors[:icon_url]).to(include("must be a valid URL"))
end
describe 'redirection uri' do
it 'rejects if empty' do
app = OauthApp.new
expect(app).to_not(be_valid)
expect(app.errors[:redirect_uris]).to(include("can't be blank"))
end
it 'rejects if invalid' do
app = OauthApp.new(redirect_uris: ['"invalid"'])
expect(app).to_not(be_valid)
expect(app.errors[:redirect_uris]).to(include('must be valid'))
end
it 'rejects if non-absolute' do
app = OauthApp.new(redirect_uris: ['//wadus.com/path'])
expect(app).to_not(be_valid)
expect(app.errors[:redirect_uris]).to(include('must be absolute'))
app = OauthApp.new(redirect_uris: ['/some_path'])
expect(app).to_not(be_valid)
expect(app.errors[:redirect_uris]).to(include('must be absolute'))
end
it 'rejects if non-https' do
app = OauthApp.new(redirect_uris: ['http://wadus.com/path'])
expect(app).to_not(be_valid)
expect(app.errors[:redirect_uris]).to(include('must be https'))
app = OauthApp.new(redirect_uris: ['file://some_path'])
expect(app).to_not(be_valid)
expect(app.errors[:redirect_uris]).to(include('must be https'))
end
it 'rejects if has fragment' do
app = OauthApp.new(redirect_uris: ['https://wad.us/?query#fragment'])
expect(app).to_not(be_valid)
expect(app.errors[:redirect_uris]).to(include('must not contain a fragment'))
end
it 'accepts if valid' do
app = OauthApp.new(redirect_uris: ['https://wad.us/path?query=value'])
app.valid?
expect(app.errors[:redirect_uris]).to(be_empty)
end
end
it 'accepts if valid' do
app = OauthApp.new(user: @user,
name: 'name',
redirect_uris: ['https://re.dir'],
icon_url: 'http://localhost/some.png',
website_url: 'http://localhost')
expect(app).to(be_valid)
end
it 'accepts without icon_url' do
app = OauthApp.create(user: @user,
name: 'name',
redirect_uris: ['https://re.dir'],
website_url: 'http://localhost')
expect(app).to(be_valid)
end
it 'accepts with no user if avoid_sync_central and central enabled' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
app = OauthApp.new(name: 'name',
redirect_uris: ['https://re.dir'],
icon_url: 'http://localhost/some.png',
website_url: 'http://localhost',
avoid_sync_central: true)
expect(app).to(be_valid)
Cartodb::Central.unstub(:sync_data_with_cartodb_central?)
end
end
context 'Central sync' do
before(:all) do
@user_oauth = FactoryGirl.create(:carto_user)
end
before(:each) do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@oauth_app = FactoryGirl.create(:oauth_app, user: @user_oauth, avoid_sync_central: false)
end
after(:each) do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@oauth_app.destroy! if @oauth_app
@oauth_app2.destroy! if @oauth_app2
end
after(:all) do
@user_oauth.destroy!
end
describe '#create' do
it 'creates app in clouds from Central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
params = { id: '26da639b-0b8c-4e81-aeb4-33b81fd0cacb',
name: 'name1',
redirect_uris: ['https://re.dir'],
icon_url: 'http://localhost/some.png',
website_url: 'http://localhost',
description: nil,
client_id: '1234',
client_secret: '5678',
restricted: false }
Cartodb::Central.any_instance
.expects(:create_oauth_app)
.with(@user_oauth.username,
params)
.returns({})
.once
expect {
@oauth_app2 = OauthApp.new(params.merge(user: @user_oauth))
@oauth_app2.id = params[:id]
@oauth_app2.save!
}.to change { OauthApp.count }.by(1)
end
it 'creates app if user not present and avoid_sync_central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central.any_instance.expects(:create_oauth_app).never
expect {
@oauth_app2 = OauthApp.create!(name: 'name1',
redirect_uris: ['https://re.dir'],
icon_url: 'http://localhost/some.png',
website_url: 'http://localhost',
avoid_sync_central: true)
}.to change { OauthApp.count }.by(1)
end
it 'creates app if Central is disabled' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
Cartodb::Central.any_instance.expects(:create_oauth_app).never
expect {
@oauth_app2 = OauthApp.create!(user: @user_oauth,
name: 'name1',
redirect_uris: ['https://re.dir'],
website_url: 'http://localhost',
icon_url: 'http://localhost/some.png')
}.to change { OauthApp.count }.by(1)
end
it 'raises error if Central is disabled and no user' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
Cartodb::Central.any_instance.expects(:create_oauth_app).never
expect {
@oauth_app2 = OauthApp.create!(name: 'name1',
redirect_uris: ['https://re.dir'],
website_url: 'http://localhost',
icon_url: 'http://localhost/some.png')
}.to raise_error
end
end
describe '#update' do
it 'updates app in clouds from Central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central.any_instance
.expects(:update_oauth_app)
.with(@user_oauth.username,
@oauth_app.id,
id: @oauth_app.id,
name: 'updated',
client_id: @oauth_app.client_id,
client_secret: @oauth_app.client_secret,
redirect_uris: @oauth_app.redirect_uris,
icon_url: @oauth_app.icon_url,
website_url: @oauth_app.website_url,
description: @oauth_app.description,
restricted: @oauth_app.restricted)
.returns({})
.once
expect {
@oauth_app.name = 'updated'
@oauth_app.save!
}.to_not raise_error
@oauth_app.reload.name.should eq 'updated'
end
it 'updates app if Central is disabled' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
Cartodb::Central.any_instance.expects(:update_oauth_app).never
expect {
@oauth_app.name = 'updated'
@oauth_app.save!
}.to_not raise_error
@oauth_app.reload.name.should eq 'updated'
end
it 'updates app if Central is avoid_sync_central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central.any_instance.expects(:update_oauth_app).never
@oauth_app.avoid_sync_central = true
expect {
@oauth_app.name = 'updated'
@oauth_app.save!
}.to_not raise_error
@oauth_app.reload.name.should eq 'updated'
end
it 'updates app to no user with avoid_sync_central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central.any_instance.expects(:update_oauth_app).never
@oauth_app.avoid_sync_central = true
expect {
@oauth_app.user = nil
@oauth_app.save!
}.to_not raise_error
@oauth_app.reload.user.should be_nil
end
end
describe '#destroy' do
after(:each) do
::Resque.unstub(:enqueue)
end
it 'does not send notification if destroying app with no users' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
::Resque.expects(:enqueue)
.with(::Resque::UserJobs::Notifications::Send, anything, anything)
.never
expect {
@oauth_app.destroy!
}.to change { OauthApp.count }.by(-1)
end
it 'sends notification if destroying app with users' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@app_user = Carto::OauthAppUser.create!(user_id: @oauth_app.user.id, oauth_app: @oauth_app)
::Resque.expects(:enqueue)
.with(::Resque::UserJobs::Notifications::Send, [@app_user.user.id], anything)
.once
expect {
@oauth_app.destroy!
}.to change { OauthApp.count }.by(-1)
end
it 'does not send notification if avoid_send_notification' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@app_user = Carto::OauthAppUser.create!(user_id: @oauth_app.user.id, oauth_app: @oauth_app)
::Resque.expects(:enqueue)
.with(::Resque::UserJobs::Notifications::Send, [@app_user.user.id], anything)
.never
expect {
@oauth_app.avoid_send_notification = true
@oauth_app.destroy!
}.to change { OauthApp.count }.by(-1)
end
it 'logs notification errors on destroy' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@app_user = Carto::OauthAppUser.create!(user_id: @oauth_app.user.id, oauth_app: @oauth_app)
error_message = "Couldn't notify users about oauth_app '#{@oauth_app.name}' deletion"
::Resque.stubs(:enqueue).raises('unknown error')
CartoDB::Logger.expects(:warning)
.with(has_entry(message: error_message))
.at_least_once
expect {
@oauth_app.destroy!
}.to raise_error(/unknown error/)
end
it 'deletes app in clouds from Central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central.any_instance
.expects(:delete_oauth_app)
.with(@user_oauth.username, @oauth_app.id)
.returns({})
.once
expect {
@oauth_app.destroy!
}.to change { OauthApp.count }.by(-1)
end
it 'deletes app if Central is disabled' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
Cartodb::Central.any_instance.expects(:delete_oauth_app).never
expect {
@oauth_app.destroy!
}.to change { OauthApp.count }.by(-1)
end
end
end
it 'fills client id and secret automatically' do
app = OauthApp.new
app.save
expect(app.client_id).to(be_present)
expect(app.client_secret).to(be_present)
end
end
end
+738
View File
@@ -0,0 +1,738 @@
require 'spec_helper_min'
require 'helpers/database_connection_helper'
module Carto
describe OauthAppUser do
include_context 'organization with users helper'
include CartoDB::Factories
include DatabaseConnectionHelper
describe 'validation' do
before(:all) do
user = FactoryGirl.create(:valid_user)
@user = Carto::User.find(user.id)
@app = FactoryGirl.create(:oauth_app, user: @user)
end
after(:all) do
@user.destroy
@app.destroy
end
it 'requires user' do
app_user = OauthAppUser.new
expect(app_user).to_not(be_valid)
expect(app_user.errors[:user]).to(include("can't be blank"))
end
it 'requires oauth app_user' do
app_user = OauthAppUser.new
expect(app_user).to_not(be_valid)
expect(app_user.errors[:oauth_app]).to(include("can't be blank"))
end
it 'does not allow duplicates' do
begin
@app_user1 = OauthAppUser.create!(user: @user, oauth_app: @app)
app_user2 = OauthAppUser.new(user: @user, oauth_app: @app)
expect(app_user2).to_not(be_valid)
expect(app_user2.errors[:user]).to(include("has already been taken"))
ensure
@app_user1.destroy if @app_user1
end
end
it 'does not accept invalid scopes' do
app_user = OauthAppUser.new(scopes: ['wadus'])
expect(app_user).to_not(be_valid)
expect(app_user.errors[:scopes]).to(include("contains unsupported scopes: wadus"))
end
it 'validates' do
app_user = OauthAppUser.new(user: @user, oauth_app: @app)
expect(app_user).to(be_valid)
end
describe 'restricted app' do
before(:all) do
@app.update!(restricted: true)
end
before(:each) do
@app.oauth_app_organizations.each(&:destroy!)
@app.oauth_app_organizations.create!(organization: @carto_organization, seats: 1)
end
after(:all) do
@app.destroy
end
it 'does not accept non-organization users' do
app_user = OauthAppUser.new(user: @user, oauth_app: @app)
expect(app_user).not_to(be_valid)
expect(app_user.errors[:user]).to(include("is not part of an organization"))
end
it 'does not accept users from unknown organizations' do
@app.oauth_app_organizations.each(&:destroy!)
@app.oauth_app_organizations.create!(organization: @carto_organization_2, seats: 1)
app_user = OauthAppUser.new(user: @carto_org_user_1, oauth_app: @app)
expect(app_user).not_to(be_valid)
expect(app_user.errors[:user]).to(include("is part of an organization which is not allowed access to this application"))
end
it 'accepts users from the authorized organization' do
app_user = OauthAppUser.new(user: @carto_org_user_1, oauth_app: @app)
expect(app_user).to(be_valid)
end
it 'does not accepts users over the seat limit' do
OauthAppUser.create!(user: @carto_org_user_1, oauth_app: @app)
app_user = OauthAppUser.new(user: @carto_org_user_2, oauth_app: @app)
expect(app_user).not_to(be_valid)
expect(app_user.errors[:user]).to(include("does not have an available seat to use this application"))
end
end
end
describe '#authorized?' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
end
before(:each) do
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@t1 = create_table(user_id: @carto_user.id)
@t2 = create_table(user_id: @carto_user.id)
end
after(:each) do
@t1.destroy
@t2.destroy
@app.destroy
end
after(:all) do
@user.destroy
@carto_user.destroy
end
it 'is authorized only if all requested scopes are already granted' do
o1 = "datasets:rw:#{@carto_user.database_schema}.#{@t1.name}"
o2 = "datasets:rw:#{@carto_user.database_schema}.#{@t2.name}"
o3 = "schemas:c:#{@carto_user.database_schema}"
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: [o1, o2, o3])
expect(oau).to(be_authorized([o1]))
expect(oau).to(be_authorized([o2]))
expect(oau).to(be_authorized([o3]))
expect(oau).to(be_authorized([o1, o2, o3]))
expect(oau).not_to(be_authorized(['not_allowed']))
expect(oau).not_to(be_authorized([o1, 'not_allowed']))
end
it 'should be authorized if requesting read permission having read-write' do
write_read = "datasets:rw:#{@t1.name}"
read = "datasets:r:#{@t1.name}"
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: [write_read])
expect(oau).to(be_authorized([read]))
end
it 'should NOT be authorized if requesting read-write permission having only read' do
write_read = "datasets:rw:#{@t1.name}"
read = "datasets:r:#{@t1.name}"
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: [read])
expect(oau).not_to(be_authorized([write_read]))
end
end
describe '#upgrade!' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@t1 = create_table(user_id: @carto_user.id)
@t2 = create_table(user_id: @carto_user.id)
@t3 = create_table(user_id: @carto_user.id)
@t4 = create_table(user_id: @carto_user.id)
end
after(:all) do
@t1.destroy
@t2.destroy
@t3.destroy
@t4.destroy
@app.destroy
@user.destroy
@carto_user.destroy
end
it 'grants all new scopes without duplicates' do
o1 = "datasets:rw:#{@t1.name}"
o2 = "datasets:rw:#{@t2.name}"
o3 = "datasets:rw:#{@t3.name}"
o4 = "datasets:rw:#{@t4.name}"
o5 = "schemas:c:#{@carto_user.database_schema}"
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: [o1, o2])
oau.upgrade!([])
expect(oau.scopes).to(eq([o1, o2]))
oau.upgrade!([o1])
expect(oau.scopes).to(eq([o1, o2]))
oau.upgrade!([o3])
expect(oau.scopes).to(eq([o1, o2, o3]))
oau.upgrade!([o2, o4])
expect(oau.scopes).to(eq([o1, o2, o3, o4]))
oau.upgrade!([])
expect(oau.scopes).to(eq([o1, o2, o3, o4]))
oau.upgrade!([o5])
expect(oau.scopes).to(eq([o1, o2, o3, o4, o5]))
end
end
describe 'datasets scope' do
before(:each) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@table1 = create_table(user_id: @carto_user.id)
end
after(:each) do
@table1.destroy
@app.destroy
@user.destroy
@carto_user.destroy
end
it 'creation and update' do
table2 = create_table(user_id: @carto_user.id)
dataset_scope1 = "datasets:rw:#{@table1.name}"
dataset_scope2 = "datasets:r:#{table2.name}"
scopes = ['user:profile', dataset_scope1, dataset_scope2]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes)
expect(oau.scopes).to(eq(scopes))
oau.upgrade!([])
expect(oau.scopes).to(eq(scopes))
oau.upgrade!([dataset_scope1])
expect(oau.scopes).to(eq(scopes))
table2.destroy
oau.destroy
end
it 'rename table and check how it affects the scopes' do
scopes_before = ['user:profile', "datasets:rw:#{@table1.name}"]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes_before)
expect(oau.all_scopes).to(eq(scopes_before))
@table1.name = 'table_renamed_' + @table1.name
@table1.save
expect(oau.all_scopes).to_not(eq(scopes_before))
scopes_after = ['user:profile', "datasets:rw:#{@table1.name}"]
expect(oau.all_scopes).to(eq(scopes_after))
oau.destroy
end
it 'write on table with the proper permissions' do
scopes_before = ['user:profile', "datasets:rw:#{@table1.name}"]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes_before)
expect(oau.all_scopes).to(eq(scopes_before))
access_token = OauthAccessToken.create!(oauth_app_user: oau, scopes: scopes_before)
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("insert into #{@table1.name} (cartodb_id) values (999)")
connection.execute("select cartodb_id from #{@table1.name}") do |result|
result[0]['cartodb_id'].should eq '999'
end
end
oau.destroy
end
it 'should fail if we change the write permissions and we try to write in the table' do
scopes_before = ['offline', 'user:profile', "datasets:rw:#{@table1.name}"]
scopes_after = ['offline', 'user:profile']
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes_before)
expect(oau.all_scopes).to(eq(scopes_before))
refresh_token = oau.oauth_refresh_tokens.create!(scopes: scopes_before)
access_token = refresh_token.exchange!(requested_scopes: scopes_before)[0]
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("insert into #{@table1.name} (cartodb_id) values (999)")
connection.execute("select cartodb_id from #{@table1.name}") do |result|
result[0]['cartodb_id'].should eq '999'
end
end
access_token_new = refresh_token.exchange!(requested_scopes: scopes_after)[0]
expect(access_token.api_key.db_role).to_not(eq(access_token_new.api_key.db_role))
with_connection_from_api_key(access_token_new.api_key) do |connection|
expect {
connection.execute("insert into #{@table1.name} (cartodb_id) values (1000)")
}.to raise_exception(Sequel::DatabaseError, /permission denied for (relation|table) #{@table1.name}/)
end
oau.destroy
end
it 'should let downgrade scope for datasets from rw to r scope' do
scopes_before = ['offline', 'user:profile', "datasets:rw:#{@table1.name}"]
scopes_after = ['offline', 'user:profile', "datasets:r:#{@table1.name}"]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes_before)
expect(oau.all_scopes).to(eq(scopes_before))
refresh_token = oau.oauth_refresh_tokens.create!(scopes: scopes_before)
access_token = refresh_token.exchange!(requested_scopes: scopes_before)[0]
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("insert into #{@table1.name} (cartodb_id) values (999)")
connection.execute("select cartodb_id from #{@table1.name}") do |result|
result[0]['cartodb_id'].should eq '999'
end
end
access_token_new = refresh_token.exchange!(requested_scopes: scopes_after)[0]
expect(access_token.api_key.db_role).to_not(eq(access_token_new.api_key.db_role))
with_connection_from_api_key(access_token_new.api_key) do |connection|
connection.execute("select cartodb_id from #{@table1.name}") do |result|
result[0]['cartodb_id'].should eq '999'
end
expect {
connection.execute("insert into #{@table1.name} (cartodb_id) values (999)")
}.to raise_exception(Sequel::DatabaseError, /permission denied for (relation|table) #{@table1.name}/)
end
oau.destroy
end
end
describe 'schemas scope' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@table1 = create_table(user_id: @carto_user.id)
@table2 = create_table(user_id: @carto_user.id)
end
after(:all) do
@table1.destroy
@table2.destroy
@app.destroy
@user.destroy
@carto_user.destroy
end
it 'creation and update' do
table2 = create_table(user_id: @carto_user.id)
dataset_scope1 = "datasets:rw:#{@table1.name}"
dataset_scope2 = "datasets:r:#{table2.name}"
scopes = ['user:profile', dataset_scope1, dataset_scope2]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes)
expect(oau.scopes).to(eq(scopes))
oau.upgrade!([])
expect(oau.scopes).to(eq(scopes))
oau.upgrade!([dataset_scope1])
expect(oau.scopes).to(eq(scopes))
table2.destroy
oau.destroy
end
it 'create table with permissions should work and assign it to the owner_role' do
schemas_scope = "schemas:c"
scopes = ['user:profile', schemas_scope]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes)
expect(oau.scopes).to(eq(scopes))
access_token = OauthAccessToken.create!(oauth_app_user: oau, scopes: scopes)
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("create table test_table as select 1 as test")
connection.execute("select count(1) from test_table") do |result|
result[0]['count'].should eq '1'
end
connection.execute("select pg_catalog.pg_get_userbyid(relowner) as owner from pg_class where relname = 'test_table'") do |result|
result[0]['owner'].should eq oau.ownership_role_name
end
connection.execute("drop table test_table")
end
oau.destroy
end
it 'create table without permissions should fail' do
scopes = ['user:profile']
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes)
expect(oau.scopes).to(eq(scopes))
access_token = OauthAccessToken.create!(oauth_app_user: oau, scopes: scopes)
with_connection_from_api_key(access_token.api_key) do |connection|
expect {
connection.execute("create table test_table as select 1 as test")
}.to raise_exception(Sequel::DatabaseError, /permission denied for schema public/)
end
oau.destroy
end
it 'create table with permission, then refresh token and drop the table with the new db role' do
schemas_scope = "schemas:c"
scopes = ['offline', 'user:profile', schemas_scope]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes)
expect(oau.scopes).to(eq(scopes))
refresh_token = oau.oauth_refresh_tokens.create!(scopes: scopes)
access_token = refresh_token.exchange!(requested_scopes: scopes)[0]
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("create table test_table as select 1 as test")
connection.execute("select count(1) from test_table") do |result|
result[0]['count'].should eq '1'
end
connection.execute("select pg_catalog.pg_get_userbyid(relowner) as owner from pg_class where relname = 'test_table'") do |result|
result[0]['owner'].should eq oau.ownership_role_name
end
end
access_token_new = refresh_token.exchange!(requested_scopes: scopes)[0]
expect(access_token.api_key.db_role).to_not(eq(access_token_new.api_key.db_role))
with_connection_from_api_key(access_token_new.api_key) do |connection|
connection.execute("drop table test_table")
end
oau.destroy
end
it 'create table with permission, then refresh token and remove permission, then try to create another table and get exception' do
schemas_scope = "schemas:c"
scopes = ['offline', 'user:profile', schemas_scope]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes)
expect(oau.scopes).to(eq(scopes))
refresh_token = oau.oauth_refresh_tokens.create!(scopes: scopes)
access_token = refresh_token.exchange!(requested_scopes: scopes)[0]
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("create table test_table as select 1 as test")
connection.execute("select count(1) from test_table") do |result|
result[0]['count'].should eq '1'
end
connection.execute("select pg_catalog.pg_get_userbyid(relowner) as owner from pg_class where relname = 'test_table'") do |result|
result[0]['owner'].should eq oau.ownership_role_name
end
connection.execute("drop table test_table")
end
access_token_new = refresh_token.exchange!(requested_scopes: ['offline', 'user:profile'])[0]
expect(access_token.api_key.db_role).to_not(eq(access_token_new.api_key.db_role))
with_connection_from_api_key(access_token_new.api_key) do |connection|
expect {
connection.execute("create table test_table_without_permissions as select 1 as test")
}.to raise_exception(Sequel::DatabaseError, /permission denied for schema public/)
end
oau.destroy
end
it 'master role can drop tables created with access token API key' do
schemas_scope = "schemas:c"
scopes = ['offline', 'user:profile', schemas_scope]
oau = OauthAppUser.create!(user: @carto_user, oauth_app: @app, scopes: scopes)
expect(oau.scopes).to(eq(scopes))
refresh_token = oau.oauth_refresh_tokens.create!(scopes: scopes)
access_token = refresh_token.exchange!(requested_scopes: scopes)[0]
with_connection_from_api_key(access_token.api_key) do |connection|
connection.execute("create table test_table as select 1 as test")
connection.execute("select count(1) from test_table") do |result|
result[0]['count'].should eq '1'
end
end
with_connection_from_api_key(@carto_user.api_keys.master.first) do |connection|
connection.execute("drop table test_table")
connection.execute("select * from pg_class where relname = 'test_table'") do |result|
result.count.should eq 0
end
end
end
end
describe 'shared datasets' do
before :each do
@app = FactoryGirl.create(:oauth_app, user: @carto_org_user_1)
@shared_table = create_table(user_id: @carto_org_user_1.id)
not_shared_table = create_table(user_id: @carto_org_user_1.id)
perm = @shared_table.table_visualization.permission
perm.acl = [{ type: 'user', entity: { id: @carto_org_user_2.id }, access: 'rw' }]
perm.save!
@shared_dataset_scope = "datasets:r:#{@carto_org_user_1.database_schema}.#{@shared_table.name}"
@non_shared_dataset_scope = "datasets:r:#{@carto_org_user_1.database_schema}.#{not_shared_table.name}"
end
after :each do
@app.destroy
end
it 'works with shared dataset' do
oau = OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app, scopes: [@shared_dataset_scope])
expect(oau.all_scopes).to(eq([@shared_dataset_scope]))
end
it 'should fail with non shared dataset' do
expect {
OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app, scopes: [@non_shared_dataset_scope])
}.to raise_error(Carto::OauthProvider::Errors::InvalidScope)
end
it 'should fail with shared and non shared dataset' do
expect {
OauthAppUser.create!(
user: @carto_org_user_2,
oauth_app: @app,
scopes: [@shared_dataset_scope, @non_shared_dataset_scope]
)
}.to raise_error(Carto::OauthProvider::Errors::InvalidScope)
end
it 'should revoke permissions removing shared permissions' do
oau = OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app, scopes: [@shared_dataset_scope])
expect(oau.all_scopes).to(eq([@shared_dataset_scope]))
expect(oau.authorized?([@shared_dataset_scope])).to eq(true)
expect(oau.authorized?([@non_shared_dataset_scope])).to eq(false)
# remove shared permissions
@shared_table.table_visualization.reload
perm = @shared_table.table_visualization.permission
perm.acl = [{ type: 'user', entity: { id: @carto_org_user_2.id }, access: 'r' }]
perm.save!
shared_dataset_scope_rw = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@shared_table.name}"
expect(oau.authorized?([@shared_dataset_scope])).to eq(true)
expect(oau.authorized?([shared_dataset_scope_rw])).to eq(false)
# remove shared permissions
@shared_table.table_visualization.reload
perm = @shared_table.table_visualization.permission
perm.acl = []
perm.save!
expect(oau.authorized?([@shared_dataset_scope])).to eq(false)
expect(oau.authorized?([@non_shared_dataset_scope])).to eq(false)
end
describe 'read - write permissions' do
before :each do
@only_read_table = create_table(user_id: @carto_org_user_1.id)
perm = @only_read_table.table_visualization.permission
perm.acl = [{ type: 'user', entity: { id: @carto_org_user_2.id }, access: 'r' }]
perm.save!
end
after :each do
@only_read_table.destroy
end
it 'should fail write scope in shared dataset with only read perms' do
rw_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@only_read_table.name}"
expect {
OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app, scopes: [rw_scope])
}.to raise_error(Carto::OauthProvider::Errors::InvalidScope)
end
end
describe 'organization shared datasets' do
before :each do
@org_shared_table = create_table(user_id: @carto_org_user_1.id)
non_org_shared_table = create_table(user_id: @carto_org_user_1.id)
perm = @org_shared_table.table_visualization.permission
perm.acl = [
{
type: Permission::TYPE_ORGANIZATION,
entity: { id: @carto_organization.id },
access: Permission::ACCESS_READWRITE
}
]
perm.save!
@org_shared_dataset_scope = "datasets:r:#{@carto_org_user_1.database_schema}.#{@org_shared_table.name}"
@non_org_shared_dataset_scope = "datasets:r:#{@carto_org_user_1.database_schema}.#{non_org_shared_table.name}"
end
it 'works with org shared dataset' do
oau = OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app, scopes: [@org_shared_dataset_scope])
expect(oau.all_scopes).to(eq([@org_shared_dataset_scope]))
end
it 'should fail with non org shared dataset' do
expect {
OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app, scopes: [@non_org_shared_dataset_scope])
}.to raise_error(Carto::OauthProvider::Errors::InvalidScope)
end
it 'should fail with org shared and non org shared dataset' do
expect {
OauthAppUser.create!(
user: @carto_org_user_2,
oauth_app: @app,
scopes: [@org_shared_dataset_scope, @non_org_shared_dataset_scope]
)
}.to raise_error(Carto::OauthProvider::Errors::InvalidScope)
end
describe 'read - write permissions' do
before :each do
@only_read_table = create_table(user_id: @carto_org_user_1.id)
perm = @only_read_table.table_visualization.permission
perm.acl = [
{
type: Permission::TYPE_ORGANIZATION,
entity: { id: @carto_organization.id },
access: Permission::ACCESS_READONLY
}
]
perm.save!
end
after :each do
@only_read_table.destroy
end
it 'should fail write scope in org shared dataset with only read perms' do
rw_scope = "datasets:rw:#{@carto_org_user_1.database_schema}.#{@only_read_table.name}"
expect {
OauthAppUser.create!(user: @carto_org_user_2, oauth_app: @app, scopes: [rw_scope])
}.to raise_error(Carto::OauthProvider::Errors::InvalidScope)
end
end
end
end
describe 'views' do
before :all do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@user_table = create_table(user_id: @carto_user.id)
@view_name = "#{@user_table.name}_view"
@materialized_view_name = "#{@user_table.name}_matview"
@carto_user.in_database do |db|
query = %{
CREATE VIEW #{@view_name} AS SELECT * FROM #{@user_table.name};
CREATE MATERIALIZED VIEW #{@materialized_view_name} AS SELECT * FROM #{@user_table.name};
}
db.execute(query)
end
end
before :each do
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
end
after :each do
@app.destroy
end
after :all do
@carto_user.in_database do |db|
query = %{
DROP VIEW #{@view_name};
DROP MATERIALIZED VIEW #{@materialized_view_name};
}
db.execute(query)
end
@user_table.destroy
@user.destroy
@carto_user.destroy
end
it 'validates view scope' do
oau = OauthAppUser.create!(
user: @carto_user,
oauth_app: @app,
scopes: ["datasets:r:#{@view_name}"]
)
expect(oau.all_scopes).to(eq(["datasets:r:#{@view_name}"]))
end
it 'validates materialized view scope' do
oau = OauthAppUser.create!(
user: @carto_user,
oauth_app: @app,
scopes: ["datasets:r:#{@materialized_view_name}"]
)
expect(oau.all_scopes).to(eq(["datasets:r:#{@materialized_view_name}"]))
end
end
describe '#destroy' do
before(:each) do
@user = FactoryGirl.create(:valid_user)
@app = FactoryGirl.create(:oauth_app, user_id: @user.id)
@app_user = Carto::OauthAppUser.create!(user_id: @user.id, oauth_app: @app)
access_token = OauthAccessToken.create!(oauth_app_user: @app_user,
scopes: ["schemas:c:#{@user.database_schema}"])
@api_key = access_token.api_key
end
after(:each) do
@app.destroy
@user.destroy
end
it 'drops the created roles' do
find_role_query = "SELECT * FROM pg_roles WHERE rolname LIKE '%#{@app_user.id}'"
@user.in_database.fetch(find_role_query).count.should eq 2
@app_user.destroy
@user.in_database.fetch(find_role_query).count.should eq 0
end
it 'reassigns the ownership of created tables to the master role' do
with_connection_from_api_key(@api_key) { |db| db.execute('CREATE TABLE puxa()') }
find_owner_query = "SELECT tableowner FROM pg_tables WHERE tablename = 'puxa'"
@user.in_database.fetch(find_owner_query).first[:tableowner].should eql @app_user.ownership_role_name
@app_user.destroy
@user.in_database.fetch(find_owner_query).first[:tableowner].should eql @user.database_username
end
end
end
end
@@ -0,0 +1,87 @@
require 'spec_helper_min'
module Carto
describe OauthAuthorizationCode do
describe '#validation' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.build(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.new(user: @carto_user, oauth_app: @app)
end
after(:all) do
@app_user.destroy
@app.destroy
@user.destroy
end
it 'does not accept invalid scopes' do
authorization = OauthAuthorizationCode.new(scopes: ['wadus'])
expect(authorization).to_not(be_valid)
expect(authorization.errors[:scopes]).to(include("contains unsupported scopes: wadus"))
end
it 'validates without redirect_uri and autogenerates code' do
authorization = OauthAuthorizationCode.new(oauth_app_user: @app_user)
expect(authorization).to(be_valid)
expect(authorization.code).to(be_present)
end
it 'validates with redirect_uri and autogenerates code' do
authorization = OauthAuthorizationCode.new(oauth_app_user: @app_user, redirect_uri: ['https://redirect'])
expect(authorization).to(be_valid)
expect(authorization.code).to(be_present)
end
end
describe '#exchange!' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.create(user: @carto_user, oauth_app: @app)
end
after(:all) do
@app_user.destroy
@app.destroy
@user.destroy
end
before(:each) do
@authorization_code = @app_user.oauth_authorization_codes.create!
end
after(:each) do
@authorization_code.destroy
end
it 'fails if the code is expired' do
@authorization_code.created_at -= 10.minutes
expect { @authorization_code.exchange! }.to(raise_error(OauthProvider::Errors::InvalidGrant))
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
end
it 'creates a new api key and blanks the code' do
access_token, refresh_token = @authorization_code.exchange!
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_false)
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(refresh_token).to(be_nil)
end
it 'with offline scope creates a new access token and refresh token' do
@authorization_code.update!(scopes: ['offline'])
access_token, refresh_token = @authorization_code.exchange!
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_false)
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(refresh_token).to(be)
end
end
end
end
@@ -0,0 +1,151 @@
require 'spec_helper_min'
module Carto
describe OauthRefreshToken do
describe '#validation' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.new(user: @carto_user, oauth_app: @app)
end
after(:all) do
@app_user.destroy
@app.destroy
@user.destroy
end
it 'requires offline scope' do
refresh_token = OauthRefreshToken.new
expect(refresh_token).not_to(be_valid)
expect(refresh_token.errors[:scopes]).to(include("must contain `offline`"))
end
it 'does not accept invalid scopes' do
refresh_token = OauthRefreshToken.new(scopes: ['wadus'])
expect(refresh_token).to_not(be_valid)
expect(refresh_token.errors[:scopes]).to(include("contains unsupported scopes: wadus"))
end
it 'validates with offline scope' do
refresh_token = OauthRefreshToken.new(oauth_app_user: @app_user, scopes: ['offline'])
expect(refresh_token).to(be_valid)
end
end
describe '#exchange!' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.create(user: @carto_user, oauth_app: @app)
end
after(:all) do
@app_user.destroy
@app.destroy
@user.destroy
end
before(:each) do
@refresh_token = @app_user.oauth_refresh_tokens.create!(scopes: ['offline'])
end
after(:each) do
@refresh_token.destroy
end
it 'fails if the token is expired' do
@refresh_token.updated_at -= 1.year
@refresh_token.save!
expect { @refresh_token.exchange! }.to(raise_error(OauthProvider::Errors::InvalidGrant))
end
it 'can exchange multiple times while it has been used in the last 6 months' do
@refresh_token.exchange!
Delorean.jump(4.months)
@refresh_token.exchange!
Delorean.jump(4.months)
@refresh_token.exchange!
Delorean.jump(7.months)
expect { @refresh_token.exchange! }.to(raise_error(OauthProvider::Errors::InvalidGrant))
end
it 'creates a new access token and regenerated the code and updated_at' do
prev_token = @refresh_token.token
prev_updated_at = @refresh_token.updated_at
access_token, refresh_token = @refresh_token.exchange!
expect(access_token.api_key).to(be)
expect(access_token.api_key.type).to(eq('oauth'))
expect(access_token.scopes).to(eq(refresh_token.scopes))
expect(refresh_token).to(eq(@refresh_token))
expect(refresh_token.token).to_not(eq(prev_token))
expect(refresh_token.updated_at).to_not(eq(prev_updated_at))
end
it 'creates a new access token with reduced scopes if asked to' do
access_token, refresh_token = @refresh_token.exchange!(requested_scopes: [])
expect(access_token.scopes).to(eq([]))
expect(refresh_token.scopes).to(eq(['offline']))
end
it 'throws an error if requesting more scopes than available' do
expect { @refresh_token.exchange!(requested_scopes: ['not_there']) }.to(
raise_error(OauthProvider::Errors::InvalidScope)
)
end
end
describe '#create!' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
@carto_user = Carto::User.find(@user.id)
@app = FactoryGirl.create(:oauth_app, user: @carto_user)
@app_user = OauthAppUser.create(user: @carto_user, oauth_app: @app)
end
after(:all) do
@app_user.destroy
@app.destroy
@user.destroy
end
before(:each) do
OauthRefreshToken.send(:remove_const, 'MAX_TOKENS_PER_OAUTH_APP_USER')
OauthRefreshToken::MAX_TOKENS_PER_OAUTH_APP_USER = 3
create_tokens
end
def create_tokens
Delorean.jump(1.month) do
5.times do
@app_user.oauth_refresh_tokens.create!(scopes: ['offline'])
end
end
Delorean.back_to_the_present
3.times do
@app_user.oauth_refresh_tokens.create!(scopes: ['offline'])
end
end
it 'keeps only most recent refresh tokens per OauthAppUser' do
expect(
OauthRefreshToken.where('oauth_app_user_id = ? and updated_at < ?', @app_user.id, Time.now - 1.month).count
).to(eq(0))
end
it 'keeps a maximum number of refresh tokens per OauthAppuser' do
expect(
OauthRefreshToken.where(oauth_app_user: @app_user).count
).to(eq(OauthRefreshToken::MAX_TOKENS_PER_OAUTH_APP_USER))
end
end
end
end
+53
View File
@@ -0,0 +1,53 @@
require_relative '../../spec_helper'
require_relative '../organization_shared_examples'
require 'helpers/storage_helper'
describe Carto::Organization do
include StorageHelper
it_behaves_like 'organization models' do
before(:each) do
# INFO: forcing ActiveRecord initialization so expectations on number of queries don't count AR queries
@the_organization = Carto::Organization.where(id: @organization.id).first
@the_organization.owner
Carto::SearchTweet.count
Carto::Geocoding.count
end
def get_twitter_imports_count_by_organization_id(organization_id)
raise "id doesn't match" unless organization_id == @the_organization.id
@the_organization.twitter_imports_count
end
def get_geocoding_calls_by_organization_id(organization_id)
raise "id doesn't match" unless organization_id == @the_organization.id
@the_organization.get_geocoding_calls
end
def get_organization
@the_organization
end
end
describe '#destroy' do
before(:each) do
@organization = Carto::Organization.find(FactoryGirl.create(:organization).id)
end
it 'destroys its groups through the extension' do
Carto::Group.any_instance.expects(:destroy_group_with_extension).once
FactoryGirl.create(:carto_group, organization: @organization)
@organization.destroy
end
it 'destroys organization assets' do
bypass_storage
asset = FactoryGirl.create(:organization_asset,
organization_id: @organization.id)
@organization.destroy
Carto::Asset.exists?(asset.id).should be_false
end
end
end
+144
View File
@@ -0,0 +1,144 @@
require_relative '../../spec_helper_min'
describe Carto::Overlay do
include Carto::Factories::Visualizations
before(:all) do
@user = FactoryGirl.create(:carto_user)
@map, @table, @table_visualization, @visualization = create_full_visualization(@user)
# For this tests we want no visualization overlay
@visualization.overlays.each(&:destroy)
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
# This avoids connection leaking.
::User[@user.id].destroy
end
describe '#create' do
it 'creates a new overlay' do
overlay = @visualization.overlays.new(type: 'header', template: 'wadus', order: 0)
Carto::VisualizationInvalidationService.any_instance.expects(:invalidate).once
overlay.save.should be_true
overlay.id.should be
overlay.visualization_id.should eq @visualization.id
overlay.type.should eq 'header'
overlay.template.should eq 'wadus'
overlay.order.should eq 0
end
it 'validates unique overlays constraints' do
overlay = @visualization.overlays.new(type: 'search')
overlay.save.should be_true
overlay2 = @visualization.overlays.new(type: 'search')
overlay2.save.should be_false
end
it 'allows multiple overlays for non-unique types' do
overlay = @visualization.overlays.new(type: 'text')
overlay.save.should be_true
overlay2 = @visualization.overlays.new(type: 'text')
overlay2.save.should be_true
end
it 'allows deletion and re-creation of unique types' do
overlay = @visualization.overlays.new(type: 'fullscreen')
overlay.save.should be_true
overlay2 = @visualization.overlays.new(type: 'fullscreen')
overlay2.save.should be_false
overlay.destroy.should be_true
overlay2.save.should be_true
end
end
describe '#update' do
it 'updates overlays' do
overlay = @visualization.overlays.new(type: 'text', template: 'wadus', order: 0)
overlay.save.should be_true
overlay.template = 'image'
overlay.type = 'logo'
overlay.order = 5
Carto::VisualizationInvalidationService.any_instance.expects(:invalidate).once
overlay.save.should be_true
overlay.reload
overlay.template.should eq 'image'
overlay.type.should eq 'logo'
overlay.order.should eq 5
end
it 'validates unique overlays constraints' do
overlay = @visualization.overlays.new(type: 'zoom')
overlay.save.should be_true
overlay2 = @visualization.overlays.new(type: 'text')
overlay2.save.should be_true
overlay2.type = 'zoom'
overlay2.save.should be_false
end
end
describe '#delete' do
it 'deletes overlays' do
overlay = @visualization.overlays.new(type: 'text', template: 'wadus', order: 0)
overlay.save.should be_true
Carto::VisualizationInvalidationService.any_instance.expects(:invalidate).once
overlay.destroy.should be_true
overlay.persisted?.should be_false
end
end
describe '#hide/show' do
it 'should change options to visible = false/true' do
overlay = @visualization.overlays.new(type: 'text', options: { 'display' => true })
overlay.hidden?.should be_false
overlay.hide
overlay.options['display'].should be_false
overlay.hidden?.should be_true
overlay.show
overlay.hidden?.should be_false
overlay.options['display'].should be_true
end
end
context 'viewer users' do
before(:each) do
user = @visualization.user
user.viewer = true
user.save
@visualization.reload
end
after(:each) do
user = @visualization.user
user.viewer = false
user.save
end
it "can't create a new overlay" do
overlay = @visualization.overlays.new(type: 'header', template: 'wadus', order: 0)
overlay.save.should be_false
overlay.errors[:visualization].should eq(["Viewer users can't edit overlays"])
end
it "can't delete overlays" do
overlay = @visualization.overlays.first
overlay.destroy.should eq false
overlay.errors[:visualization].should include("Viewer users can't edit overlays")
Carto::Overlay.exists?(overlay.id).should eq true
end
end
end
+30
View File
@@ -0,0 +1,30 @@
require 'spec_helper'
require 'models/permissions_shared_examples'
describe Carto::Permission do
it_behaves_like 'permission models' do
def permission_from_visualization_id(entity_id)
visualization_from_id(entity_id).permission
end
def visualization_from_id(entity_id)
Carto::Visualization.find(entity_id)
end
def permission_klass
Carto::Permission
end
def user_for_association_from_id(user_id)
Carto::User.find(user_id)
end
def save_permission(permission)
permission.save!
end
def validation_error_klass
ActiveRecord::RecordInvalid
end
end
end
+402
View File
@@ -0,0 +1,402 @@
require 'spec_helper_min'
require 'support/helpers'
describe Carto::RateLimit do
include CartoDB::Factories
before :each do
User.any_instance.stubs(:save_rate_limits).returns(true)
@user = FactoryGirl.create(:valid_user)
@rate_limit = Carto::RateLimit.create!(maps_anonymous: Carto::RateLimitValues.new([0, 1, 2]),
maps_static: Carto::RateLimitValues.new([3, 4, 5]),
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17, 30, 32, 34]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
end
after :each do
User.any_instance.unstub(:save_rate_limits)
@user.destroy if @user
@rate_limit.destroy if @rate_limit
@rate_limit2.destroy if @rate_limit2
end
describe '#CRUD' do
it 'is persisted correctly to database' do
rate_limit = Carto::RateLimit.find(@rate_limit.id)
rate_limit.maps_anonymous.first.max_burst.should eq 0
rate_limit.maps_anonymous.first.count_per_period.should eq 1
rate_limit.maps_anonymous.first.period.should eq 2
rate_limit.maps_anonymous.first.to_array.should eq [0, 1, 2]
rate_limit.maps_static.first.to_array.should eq [3, 4, 5]
rate_limit.maps_static_named.first.to_array.should eq [6, 7, 8]
rate_limit.maps_dataview.first.to_array.should eq [9, 10, 11]
rate_limit.maps_dataview_search.first.to_array.should eq [12, 13, 14]
rate_limit.maps_analysis.first.to_array.should eq [18, 19, 20]
rate_limit.maps_tile.length.should eq 2
rate_limit.maps_tile.to_redis_array.should eq [1, 2, 17, 30, 32, 34]
rate_limit.maps_attributes.first.to_array.should eq [21, 22, 23]
rate_limit.maps_named_list.first.to_array.should eq [24, 25, 26]
rate_limit.maps_named_create.first.to_array.should eq [27, 28, 29]
rate_limit.maps_named_get.first.to_array.should eq [30, 31, 32]
rate_limit.maps_named.first.to_array.should eq [33, 34, 35]
rate_limit.maps_named_update.first.to_array.should eq [36, 37, 38]
rate_limit.maps_named_delete.first.to_array.should eq [39, 40, 41]
rate_limit.maps_named_tiles.first.to_array.should eq [10, 11, 12]
rate_limit.maps_analysis_catalog.first.to_array.should eq [10, 11, 12]
rate_limit.sql_query.first.to_array.should eq [13, 14, 15]
rate_limit.sql_query_format.first.to_array.should eq [16, 17, 18]
rate_limit.sql_job_create.first.to_array.should eq [19, 110, 111]
rate_limit.sql_job_get.first.to_array.should eq [6, 7, 8]
rate_limit.sql_job_delete.first.to_array.should eq [0, 1, 2]
rate_limit.sql_copy_from.first.to_array.should eq [1, 1, 60]
rate_limit.sql_copy_to.first.to_array.should eq [1, 1, 60]
end
it 'updates a rate_limit' do
rate_limit = Carto::RateLimit.find(@rate_limit.id)
rate_limit.maps_anonymous.first.max_burst = 1
rate_limit.maps_anonymous.first.count_per_period = 2
rate_limit.maps_anonymous.first.period = 3
rate_limit.maps_tile.each do |r|
r.max_burst += 1
r.count_per_period += 1
r.period += 1
end
rate_limit.save
rate_limit = Carto::RateLimit.find(@rate_limit.id)
rate_limit.maps_anonymous.first.max_burst.should eq 1
rate_limit.maps_anonymous.first.count_per_period.should eq 2
rate_limit.maps_anonymous.first.period.should eq 3
rate_limit.maps_tile.first.max_burst.should eq 2
rate_limit.maps_tile.first.count_per_period.should eq 3
rate_limit.maps_tile.first.period.should eq 18
rate_limit.maps_tile.second.max_burst.should eq 31
rate_limit.maps_tile.second.count_per_period.should eq 33
rate_limit.maps_tile.second.period.should eq 35
end
it 'updates a rate_limit to redis' do
User.any_instance.unstub(:save_rate_limits)
map_prefix = "limits:rate:store:#{@user.username}:maps:"
@user.rate_limit_id = @rate_limit.id
@user.save
$limits_metadata.LRANGE("#{map_prefix}anonymous", 0, 2).should == ["0", "1", "2"]
@rate_limit.maps_anonymous.first.max_burst = 1
@rate_limit.maps_anonymous.first.count_per_period = 2
@rate_limit.maps_anonymous.first.period = 3
@rate_limit.save
@rate_limit.save_to_redis(@user)
$limits_metadata.LRANGE("#{map_prefix}anonymous", 0, 2).should == ["1", "2", "3"]
end
it 'is persisted correctly to redis' do
map_prefix = "limits:rate:store:#{@user.username}:maps:"
sql_prefix = "limits:rate:store:#{@user.username}:sql:"
$limits_metadata.EXISTS("#{map_prefix}anonymous").should eq 0
$limits_metadata.EXISTS("#{map_prefix}static").should eq 0
$limits_metadata.EXISTS("#{map_prefix}static_named").should eq 0
$limits_metadata.EXISTS("#{map_prefix}dataview").should eq 0
$limits_metadata.EXISTS("#{map_prefix}dataview_search").should eq 0
$limits_metadata.EXISTS("#{map_prefix}analysis").should eq 0
$limits_metadata.EXISTS("#{map_prefix}tile").should eq 0
$limits_metadata.EXISTS("#{map_prefix}attributes").should eq 0
$limits_metadata.EXISTS("#{map_prefix}named_list").should eq 0
$limits_metadata.EXISTS("#{map_prefix}named_create").should eq 0
$limits_metadata.EXISTS("#{map_prefix}named_get").should eq 0
$limits_metadata.EXISTS("#{map_prefix}named").should eq 0
$limits_metadata.EXISTS("#{map_prefix}named_update").should eq 0
$limits_metadata.EXISTS("#{map_prefix}named_delete").should eq 0
$limits_metadata.EXISTS("#{map_prefix}named_tiles").should eq 0
$limits_metadata.EXISTS("#{map_prefix}analysis_catalog").should eq 0
$limits_metadata.EXISTS("#{sql_prefix}query").should eq 0
$limits_metadata.EXISTS("#{sql_prefix}query_format").should eq 0
$limits_metadata.EXISTS("#{sql_prefix}job_create").should eq 0
$limits_metadata.EXISTS("#{sql_prefix}job_get").should eq 0
$limits_metadata.EXISTS("#{sql_prefix}job_delete").should eq 0
$limits_metadata.EXISTS("#{sql_prefix}copy_from").should eq 0
$limits_metadata.EXISTS("#{sql_prefix}copy_to").should eq 0
@rate_limit.save_to_redis(@user)
$limits_metadata.LRANGE("#{map_prefix}anonymous", 0, 2).should == ["0", "1", "2"]
$limits_metadata.LRANGE("#{map_prefix}static", 0, 2).should == ["3", "4", "5"]
$limits_metadata.LRANGE("#{map_prefix}static_named", 0, 2).should == ["6", "7", "8"]
$limits_metadata.LRANGE("#{map_prefix}dataview", 0, 2).should == ["9", "10", "11"]
$limits_metadata.LRANGE("#{map_prefix}dataview_search", 0, 2).should == ["12", "13", "14"]
$limits_metadata.LRANGE("#{map_prefix}analysis", 0, 2).should == ["18", "19", "20"]
$limits_metadata.LRANGE("#{map_prefix}tile", 0, 5).should == ["1", "2", "17", "30", "32", "34"]
$limits_metadata.LRANGE("#{map_prefix}attributes", 0, 2).should == ["21", "22", "23"]
$limits_metadata.LRANGE("#{map_prefix}named_list", 0, 2).should == ["24", "25", "26"]
$limits_metadata.LRANGE("#{map_prefix}named_create", 0, 2).should == ["27", "28", "29"]
$limits_metadata.LRANGE("#{map_prefix}named_get", 0, 2).should == ["30", "31", "32"]
$limits_metadata.LRANGE("#{map_prefix}named", 0, 2).should == ["33", "34", "35"]
$limits_metadata.LRANGE("#{map_prefix}named_update", 0, 2).should == ["36", "37", "38"]
$limits_metadata.LRANGE("#{map_prefix}named_delete", 0, 2).should == ["39", "40", "41"]
$limits_metadata.LRANGE("#{map_prefix}named_tiles", 0, 2).should == ["10", "11", "12"]
$limits_metadata.LRANGE("#{map_prefix}analysis_catalog", 0, 2).should == ["10", "11", "12"]
$limits_metadata.LRANGE("#{sql_prefix}query", 0, 2).should == ["13", "14", "15"]
$limits_metadata.LRANGE("#{sql_prefix}query_format", 0, 2).should == ["16", "17", "18"]
$limits_metadata.LRANGE("#{sql_prefix}job_create", 0, 2).should == ["19", "110", "111"]
$limits_metadata.LRANGE("#{sql_prefix}job_get", 0, 2).should == ["6", "7", "8"]
$limits_metadata.LRANGE("#{sql_prefix}job_delete", 0, 2).should == ["0", "1", "2"]
$limits_metadata.LRANGE("#{sql_prefix}copy_from", 0, 2).should == ["1", "1", "60"]
$limits_metadata.LRANGE("#{sql_prefix}copy_to", 0, 2).should == ["1", "1", "60"]
@rate_limit.maps_static.first.max_burst = 4
@rate_limit.save_to_redis(@user)
$limits_metadata.LRANGE("#{map_prefix}static", 0, 2).should == ["4", "4", "5"]
end
it 'is removed correctly from redis' do
map_prefix = "limits:rate:store:#{@user.username}:maps:"
$limits_metadata.EXISTS("#{map_prefix}anonymous").should eq 0
@rate_limit.save_to_redis(@user)
$limits_metadata.EXISTS("#{map_prefix}anonymous").should eq 1
@rate_limit.destroy_completely(@user)
$limits_metadata.EXISTS("#{map_prefix}anonymous").should eq 0
expect {
Carto::RateLimit.find(@rate_limit.id)
}.to raise_error(ActiveRecord::RecordNotFound)
end
it 'cannot crate a rate_limit with wrong number of rate limits' do
expect {
Carto::RateLimit.create!(maps_anonymous: Carto::RateLimitValues.new([0, 1]),
maps_static: Carto::RateLimitValues.new([3, 4, 5]),
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17, 30, 32, 34]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
}.to raise_error(/Error: Number of rate limits needs to be multiple of three/)
expect {
Carto::RateLimit.create!(maps_anonymous: Carto::RateLimitValues.new([0, 1, 2]),
maps_static: Carto::RateLimitValues.new([3, 4, 5]),
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17, 30]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
}.to raise_error(/Error: Number of rate limits needs to be multiple of three/)
end
it 'raises error if endpoint is set to nil or does not exist' do
expect {
@rate_limit2 = Carto::RateLimit.create!(maps_anonymous: nil,
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
}.to raise_error(ActiveRecord::RecordInvalid)
end
it 'raises error if any endpoint values are empty' do
expect {
@rate_limit2 = Carto::RateLimit.create!(maps_anonymous: Carto::RateLimitValues.new([]),
maps_static: Carto::RateLimitValues.new([3, 4, 5]),
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
}.to raise_error(ActiveRecord::RecordInvalid)
end
it 'raises error if any endpoint values are empty' do
rate_limit_not_saved = Carto::RateLimit.new(maps_anonymous: Carto::RateLimitValues.new([]),
maps_static: Carto::RateLimitValues.new([3, 4, 5]),
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
expect {
rate_limit_not_saved.save_to_redis(@user)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it 'compares the same rate limit instance' do
@rate_limit.should eq @rate_limit
end
it 'compares two different rate limits attributes' do
@rate_limit2 = Carto::RateLimit.create!(maps_anonymous: Carto::RateLimitValues.new([1, 1, 2]),
maps_static: Carto::RateLimitValues.new([3, 4, 5]),
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17, 30, 32, 34]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
@rate_limit.should_not eq @rate_limit2
end
it 'compares two different rate limits with same attributes' do
@rate_limit2 = Carto::RateLimit.create!(maps_anonymous: Carto::RateLimitValues.new([0, 1, 2]),
maps_static: Carto::RateLimitValues.new([3, 4, 5]),
maps_static_named: Carto::RateLimitValues.new([6, 7, 8]),
maps_dataview: Carto::RateLimitValues.new([9, 10, 11]),
maps_dataview_search: Carto::RateLimitValues.new([12, 13, 14]),
maps_analysis: Carto::RateLimitValues.new([18, 19, 20]),
maps_tile: Carto::RateLimitValues.new([1, 2, 17, 30, 32, 34]),
maps_attributes: Carto::RateLimitValues.new([21, 22, 23]),
maps_named_list: Carto::RateLimitValues.new([24, 25, 26]),
maps_named_create: Carto::RateLimitValues.new([27, 28, 29]),
maps_named_get: Carto::RateLimitValues.new([30, 31, 32]),
maps_named: Carto::RateLimitValues.new([33, 34, 35]),
maps_named_update: Carto::RateLimitValues.new([36, 37, 38]),
maps_named_delete: Carto::RateLimitValues.new([39, 40, 41]),
maps_named_tiles: Carto::RateLimitValues.new([10, 11, 12]),
maps_analysis_catalog: Carto::RateLimitValues.new([10, 11, 12]),
sql_query: Carto::RateLimitValues.new([13, 14, 15]),
sql_query_format: Carto::RateLimitValues.new([16, 17, 18]),
sql_job_create: Carto::RateLimitValues.new([19, 110, 111]),
sql_job_get: Carto::RateLimitValues.new([6, 7, 8]),
sql_job_delete: Carto::RateLimitValues.new([0, 1, 2]),
sql_copy_from: Carto::RateLimitValues.new([1, 1, 60]),
sql_copy_to: Carto::RateLimitValues.new([1, 1, 60]))
@rate_limit.should eq @rate_limit2
end
end
end
@@ -0,0 +1,48 @@
require 'spec_helper_min'
module Carto
describe ReceivedNotification do
before(:all) do
@user = FactoryGirl.create(:carto_user)
end
before(:each) do
@notification = FactoryGirl.create(:notification)
@user.reload
end
after(:each) do
@notification.destroy
end
after(:all) do
@user.destroy
end
def create_received_notification(read)
@user.received_notifications.create!(
notification: @notification,
received_at: DateTime.now,
read_at: read ? DateTime.now : nil
)
end
describe 'User#unread_received_notifications' do
it 'does not list read notifications' do
unread = create_received_notification(false)
create_received_notification(true)
expect(@user.received_notifications.unread).to eq [unread]
end
it 'sorted in decreasing date order' do
unread1 = create_received_notification(false)
Delorean.jump(5.seconds)
unread2 = create_received_notification(false)
Delorean.jump(5.seconds)
unread3 = create_received_notification(false)
Delorean.back_to_the_present
expect(@user.received_notifications.unread).to eq [unread3, unread2, unread1]
end
end
end
end
+9
View File
@@ -0,0 +1,9 @@
require_relative '../shared_entity_shared_examples'
describe Carto::SharedEntity do
it_behaves_like 'shared entity models' do
def shared_entity_class
Carto::SharedEntity
end
end
end
+28
View File
@@ -0,0 +1,28 @@
require 'spec_helper_min'
describe Carto::Snapshot do
before(:all) do
bypass_named_maps
@user = FactoryGirl.create(:carto_user)
@visualization = FactoryGirl.create(:carto_visualization, user: @user)
end
after(:all) do
@visualization.destroy
@user.destroy
end
describe('#validation') do
it 'rejects nil visualization' do
snapshot = Carto::Snapshot.new(user_id: @user.id)
snapshot.save.should be_false
snapshot.errors[:visualization].should_not be_empty
end
it 'rejects nil user' do
snapshot = Carto::Snapshot.new(visualization_id: @visualization.id)
snapshot.save.should be_false
snapshot.errors[:user].should_not be_empty
end
end
end
+194
View File
@@ -0,0 +1,194 @@
require_relative '../../spec_helper'
require 'helpers/unique_names_helper'
describe Carto::Template do
include UniqueNamesHelper
include_context 'organization with users helper'
after(:each) do
Carto::Template.all.each { |template| template.delete }
delete_user_data(@org_user_owner)
end
it "Tests basic creation and FKs" do
expected_title = 'my_first_template'
expected_min_supported_version = '3.12.0'
expected_max_supported_version = '3.99.0'
expected_description = 'example description blablabla'
expected_code = '{ fake = function(e){}; }'
expected_required_tables = [ "#{@org_user_owner.database_schema}.table_1",
"#{@org_user_owner.database_schema}.table_2-1" ]
new_template = Carto::Template.new
new_template.valid?.should eq false
(new_template.errors.messages.keys - [:title, :organization_id, :source_visualization_id]).should eq []
new_template.save.should eq false
new_template.title = expected_title
# errors list is only updated upon call to valid?
new_template.valid?.should eq false
(new_template.errors.messages.keys - [:organization_id, :source_visualization_id]).should eq []
new_template.min_supported_version = expected_min_supported_version
new_template.max_supported_version = expected_max_supported_version
(new_template.errors.messages.keys - [:organization_id, :source_visualization_id]).should eq []
new_template.organization_id = @organization.id
new_template.valid?.should eq false
(new_template.errors.messages.keys - [:source_visualization_id]).should eq []
table = create_table({ privacy: UserTable::PRIVACY_PRIVATE, name: 'some_table_name', user_id: @org_user_owner.id })
another_table = create_table({ privacy: UserTable::PRIVACY_PRIVATE, name: 'some_other_table_name',
user_id: @org_user_owner.id })
expected_required_tables = [ "#{@org_user_owner.database_schema}.#{table.name}",
"#{@org_user_owner.database_schema}.#{another_table.name}" ]
new_template.source_visualization_id = table.table_visualization.id
new_template.valid?.should eq true
new_template.errors.messages.keys.should eq []
new_template.save.should eq true
new_template.required_tables.should eq []
new_template.description = expected_description
new_template.code = expected_code
new_template.required_tables = expected_required_tables
new_template.save
template = Carto::Template.where(id: new_template.id).first
template.should eq new_template
template.title.should eq expected_title
template.description.should eq expected_description
template.min_supported_version.should eq new_template.min_supported_version
template.max_supported_version.should eq new_template.max_supported_version
template.source_visualization_id.should eq new_template.source_visualization_id
template.organization_id.should eq new_template.organization_id
template.code.should eq new_template.code
template.required_tables.should eq new_template.required_tables
template.required_tables = [ 1 ]
template.valid?.should eq false
(template.errors.messages.keys - [:required_tables]).should eq []
template.required_tables = [ 'invalid name!' ]
template.valid?.should eq false
(template.errors.messages.keys - [:required_tables]).should eq []
template.required_tables = [ 'not_qualified' ]
template.valid?.should eq false
(template.errors.messages.keys - [:required_tables]).should eq []
end
it 'tests relates_to_table?() functionality' do
table = create_table(privacy: UserTable::PRIVACY_PRIVATE, name: 'table1', user_id: @org_user_owner.id)
table_vis = table.table_visualization
other_table = create_table(privacy: UserTable::PRIVACY_PRIVATE, name: 'table2', user_id: @org_user_owner.id)
template = Carto::Template.new({
title: 'title',
code: '',
min_supported_version: '1.2.3',
max_supported_version: '2.0.0',
source_visualization_id: table_vis.id,
organization_id: @org_user_owner.organization.id,
required_tables: [ "#{@org_user_owner.database_schema}.#{table.name}" ]
})
template.save.should eq true
template.relates_to_table?(table).should eq true
template.relates_to_table?(other_table).should eq false
end
it 'tests Visualization models related_templates()' do
table = create_table(privacy: UserTable::PRIVACY_PRIVATE, name: 'table1', user_id: @org_user_owner.id)
other_table = create_table(privacy: UserTable::PRIVACY_PRIVATE, name: 'table2', user_id: @org_user_owner.id)
template = Carto::Template.new({
title: 'title',
code: '',
min_supported_version: '1.2.3',
max_supported_version: '2.0.0',
source_visualization_id: table.table_visualization.id,
organization_id: @org_user_owner.organization.id,
required_tables: [ "#{@org_user_owner.database_schema}.#{table.name}" ]
})
template.save.should eq true
another_template_from_user = Carto::Template.new({
title: 'title',
code: '',
min_supported_version: '1.2.3',
max_supported_version: '2.0.0',
source_visualization_id: other_table.table_visualization.id,
organization_id: @org_user_owner.organization.id,
required_tables: [ "#{@org_user_owner.database_schema}.#{other_table.name}" ]
})
expected_templates = [ template ]
related_vis = Carto::Visualization.where(id: table.table_visualization.id).first
.related_templates.should eq expected_templates
related_vis = CartoDB::Visualization::Member.new(id: table.table_visualization.id).fetch
.related_templates.should eq expected_templates
Carto::Visualization.where(id: other_table.table_visualization.id).first
.related_templates.should eq []
CartoDB::Visualization::Member.new(id: other_table.table_visualization.id).fetch
.related_templates.should eq []
end
it 'tests you cannot see tables outside the organization' do
org2 = test_organization.save
org2_user_owner = create_test_user(unique_name('user'))
user_org = CartoDB::UserOrganization.new(org2.id, org2_user_owner.id)
user_org.promote_user_to_admin
org2.reload
org2_user_owner.reload
o_table = create_table(privacy: UserTable::PRIVACY_PRIVATE, name: 'table1', user_id: @org_user_owner.id)
o2_table = create_table(privacy: UserTable::PRIVACY_PRIVATE, name: 'table2', user_id: org2_user_owner.id)
template = Carto::Template.new({
title: 'title',
code: '',
min_supported_version: '1.2.3',
max_supported_version: '2.0.0',
source_visualization_id: o_table.table_visualization.id,
organization_id: @org_user_owner.organization.id,
required_tables: [ "#{org2_user_owner.database_schema}.#{o2_table.name}" ]
})
template.save.should eq false
(template.errors.messages.keys - [:required_tables]).should eq []
template.organization_id = org2.id
template.required_tables = [ "#{@org_user_owner.database_schema}.#{o_table.name}" ]
template.save.should eq false
# Because organization_id has changed, it must match with source_visualization_id or won't let you save
(template.errors.messages.keys - [:required_tables, :source_visualization_id]).should eq []
# But setting all ok should go ahead
template = Carto::Template.new({
title: 'title',
code: '',
min_supported_version: '1.2.3',
max_supported_version: '2.0.0',
source_visualization_id: o2_table.table_visualization.id,
organization_id: org2.id,
required_tables: [ "#{org2_user_owner.database_schema}.#{o2_table.name}" ]
})
template.save.should eq true
template.destroy
delete_user_data org2_user_owner
org2.destroy_cascade
end
end
+442
View File
@@ -0,0 +1,442 @@
require_relative '../../spec_helper'
require_relative '../../../app/models/carto/user_creation'
describe Carto::UserCreation do
describe 'autologin?' do
it 'is true for autologin_user_creation factory' do
FactoryGirl.build(:autologin_user_creation).autologin?.should == true
end
it 'is false for states other than success' do
FactoryGirl.build(:autologin_user_creation, state: 'creating_user').autologin?.should == false
FactoryGirl.build(:autologin_user_creation, state: 'validating_user').autologin?.should == false
FactoryGirl.build(:autologin_user_creation, state: 'saving_user').autologin?.should == false
FactoryGirl.build(:autologin_user_creation, state: 'promoting_user').autologin?.should == false
FactoryGirl.build(:autologin_user_creation, state: 'creating_user_in_central').autologin?.should == false
FactoryGirl.build(:autologin_user_creation, state: 'load_common_data').autologin?.should == false
FactoryGirl.build(:autologin_user_creation, state: 'failure').autologin?.should == false
FactoryGirl.build(:autologin_user_creation, state: 'success').autologin?.should == true
end
it 'is stops working after one minute' do
FactoryGirl.build(:autologin_user_creation, created_at: Time.now - 61.seconds).autologin?.should == false
FactoryGirl.build(:autologin_user_creation, created_at: Time.now - 60.seconds).autologin?.should == false
FactoryGirl.build(:autologin_user_creation, created_at: Time.now - 59.seconds).autologin?.should == true
end
it 'is false for users with enable_account_token' do
user_creation = FactoryGirl.build(:autologin_user_creation)
user = user_creation.instance_variable_get(:@cartodb_user)
user.enable_account_token = 'whatever'
user_creation.autologin?.should == false
end
it 'is false for disabled users' do
user_creation = FactoryGirl.build(:autologin_user_creation)
user = user_creation.instance_variable_get(:@cartodb_user)
user.enabled = false
user_creation.autologin?.should == false
end
it 'is false for users that have seen their dashboard' do
user_creation = FactoryGirl.build(:autologin_user_creation)
user = user_creation.instance_variable_get(:@cartodb_user)
user.dashboard_viewed_at = Time.now
user_creation.autologin?.should == false
end
end
describe 'validation token' do
include_context 'organization with users helper'
before(:each) do
# Disable central for these tests
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
end
it 'assigns an enable_account_token if user has not signed up with Google' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = false
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
saved_user = Carto::User.order("created_at desc").limit(1).first
saved_user.enable_account_token.should_not be_nil
end
it 'does not assign an enable_account_token if user has signed up with Google' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = true
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
saved_user = Carto::User.order("created_at desc").limit(1).first
saved_user.username.should == user_data.username
saved_user.enable_account_token.should be_nil
end
it 'does not assign an enable_account_token if user has signed up with GitHub' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.github_user_id = 123
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
saved_user = Carto::User.order("created_at desc").limit(1).first
saved_user.username.should == user_data.username
saved_user.enable_account_token.should be_nil
end
it 'does not assign an enable_account_token nor sends email if user had an invitation and the right token is set' do
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::NewOrganizationUser).never
::Resque.expects(:enqueue).with(Resque::OrganizationJobs::Mail::Invitation, instance_of(String)).once
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = false
invitation = Carto::Invitation.create_new(@carto_org_user_owner, [user_data.email], 'Welcome!', false)
invitation.save
user_creation = Carto::UserCreation.
new_user_signup(user_data).
with_invitation_token(invitation.token(user_data.email))
user_creation.next_creation_step until user_creation.finished?
user_creation.reload
saved_user = user_creation.user
saved_user.username.should == user_data.username
saved_user.enable_account_token.should be_nil
end
it 'with viewer invitations creates viewer users' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
user_data = FactoryGirl.build(:valid_user, organization: @organization, google_sign_in: false)
invitation = Carto::Invitation.create_new(@carto_org_user_owner, [user_data.email], 'Welcome!', true)
invitation.save
user_creation = Carto::UserCreation.
new_user_signup(user_data).
with_invitation_token(invitation.token(user_data.email))
user_creation.next_creation_step until user_creation.finished?
user_creation.reload
user_creation.user.viewer.should eq true
end
it 'neither creates a new User nor sends the mail and marks creation as failure if saving fails' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
::User.any_instance.stubs(:save).raises('saving error')
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::NewOrganizationUser).never
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = true
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
saved_user = Carto::User.where(username: user_data.username).first
saved_user.should == nil
user_creation.reload
user_creation.state.should == 'failure'
end
after(:each) do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
end
it 'neither creates a new User nor sends the mail and marks creation as failure if Central fails' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
::User.any_instance.stubs(:create_in_central).raises('Error on state creating_user_in_central, mark_as_failure: false. Error: Application server responded with http 422: {"errors":["Existing username."]}')
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::NewOrganizationUser).never
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = true
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
saved_user = Carto::User.where(username: user_data.username).first
saved_user.should == nil
user_creation.reload
user_creation.state.should == 'failure'
end
it 'neither creates a new User nor sends the mail and marks creation as failure if Central has a registered user matching username' do
user = prepare_fake_central_user
# This tests only matching usernames
user.email = 'other@whatever.com'
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::NewOrganizationUser).never
user.organization = @organization
user_creation = Carto::UserCreation.new_user_signup(user)
user_creation.next_creation_step until user_creation.finished?
Carto::User.where(username: user.username).first.should == nil
user_creation.reload
user_creation.state.should == 'failure'
end
it 'neither creates a new User nor sends the mail and marks creation as failure if Central has a registered user matching email' do
user = prepare_fake_central_user
# This tests only matching emails
user.username = 'other_whatever'
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::NewOrganizationUser).never
user.organization = @organization
user_creation = Carto::UserCreation.new_user_signup(user)
user_creation.next_creation_step until user_creation.finished?
Carto::User.where(username: user.username).first.should == nil
user_creation.reload
user_creation.state.should == 'failure'
end
it 'neither creates a new User nor sends the mail and marks creation as failure if username es empty' do
user = prepare_fake_central_user
# This tests only matching emails
user.username = nil
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::NewOrganizationUser).never
user.organization = @organization
expect {
user_creation = Carto::UserCreation.new_user_signup(user)
}.to raise_error
end
def prepare_fake_central_user
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
fake_central_client = {}
fake_central_client.stubs(:create_organization_user).returns(true)
::User.any_instance.stubs(:cartodb_central_client).returns(fake_central_client)
Cartodb::Central.stubs(:new).returns(fake_central_client)
user = FactoryGirl.build(:valid_user)
central_user_data = JSON.parse(user.to_json)
# Central doesn't return exactly the same attributes, but this is good enough for testing
Cartodb::Central.any_instance.stubs(:get_user).returns(central_user_data)
fake_central_client.stubs(:get_user).returns(central_user_data)
user
end
end
describe 'validation email' do
include_context 'organization with users helper'
# INFO : this mail contains validation link
it 'triggers a ::Resque::UserJobs::Mail::NewOrganizationUser' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
::Resque.expects(:enqueue).with(Resque::UserJobs::Mail::NewOrganizationUser, instance_of(String)).once
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = false
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
end
it 'should trigger load_common_data in the user if common_data_url is setted' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
::User.any_instance.expects(:load_common_data).with('http://www.example.com').once
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = false
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.set_common_data_url("http://www.example.com")
user_creation.next_creation_step until user_creation.finished?
end
it 'should not trigger load_common_data in the user if common_data_url is not setted' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
::User.any_instance.expects(:load_common_data).with('http://www.example.com').never
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_data.google_sign_in = false
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
end
it 'should send invitation email but not validation email if user is created via API' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
::Resque.expects(:enqueue).with(Resque::OrganizationJobs::Mail::Invitation, instance_of(String)).never
::Resque.expects(:enqueue).with(Resque::UserJobs::Mail::NewOrganizationUser, instance_of(String)).once
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
user_creation = Carto::UserCreation.new_user_signup(user_data, Carto::UserCreation::CREATED_VIA_API)
user_creation.next_creation_step until user_creation.finished?
end
end
describe 'organization overquota email' do
include_context 'organization with users helper'
it 'triggers a DiskQuotaLimitReached mail if organization has run out of quota for new users' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
::Resque.expects(:enqueue).with(Resque::UserJobs::Mail::NewOrganizationUser, instance_of(String)).once
::Resque.expects(:enqueue).with(Resque::OrganizationJobs::Mail::DiskQuotaLimitReached, instance_of(String)).once
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
@organization.quota_in_bytes = @organization.assigned_quota + @organization.default_quota_in_bytes + 1
@organization.save
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
end
end
describe 'organization over seats email' do
include_context 'organization with users helper'
it 'triggers a SeatLimitReached mail if organization has run out of seats new users' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
::Resque.expects(:enqueue).with(Resque::UserJobs::Mail::NewOrganizationUser, instance_of(String)).once
::Resque.expects(:enqueue).with(Resque::OrganizationJobs::Mail::SeatLimitReached, instance_of(String)).once
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
@organization.seats = 4
@organization.save
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
end
it 'doesnt trigger any unexpected mails if organization is ok' do
::User.any_instance.stubs(:create_in_central).returns(true)
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
::Resque.expects(:enqueue).with(Resque::UserJobs::Mail::NewOrganizationUser, instance_of(String)).once
::Resque.expects(:enqueue).with(Resque::OrganizationJobs::Mail::SeatLimitReached, instance_of(String)).never
::Resque.expects(:enqueue).with(Resque::OrganizationJobs::Mail::DiskQuotaLimitReached, instance_of(String)).never
user_data = FactoryGirl.build(:valid_user)
user_data.organization = @organization
@organization.seats = 15
@organization.save
user_creation = Carto::UserCreation.new_user_signup(user_data)
user_creation.next_creation_step until user_creation.finished?
end
end
describe '#initialize_user' do
it 'initializes users with http_authentication without organization' do
created_via = Carto::UserCreation::CREATED_VIA_HTTP_AUTENTICATION
user = FactoryGirl.build(:valid_user)
user.organization_id.should == nil
user_creation = Carto::UserCreation.new_user_signup(user, created_via)
initialized_user = user_creation.send(:initialize_user)
initialized_user.should_not be_nil
initialized_user.organization_id.should be_nil
end
end
describe 'state machine' do
before(:each) do
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
created_via = Carto::UserCreation::CREATED_VIA_HTTP_AUTENTICATION
user = FactoryGirl.build(:valid_user)
@user_creation = Carto::UserCreation.new_user_signup(user, created_via)
end
after(:each) do
@user_creation.user.destroy
end
def creation_steps(user_creation)
states = [user_creation.state]
until user_creation.finished?
user_creation.next_creation_step
states << user_creation.state
end
states
end
it 'with Central and builder, does all the steps' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
User.any_instance.stubs(:validate_credentials_not_taken_in_central).returns(true)
@user_creation.expects(:create_in_central).once
@user_creation.expects(:load_common_data).once
creation_steps(@user_creation).should eq ["enqueuing", "creating_user", "validating_user", "saving_user",
"promoting_user", "creating_user_in_central", "load_common_data",
"success"]
end
it 'without Central, skips creation in central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@user_creation.expects(:create_in_central).never
@user_creation.expects(:load_common_data).once
creation_steps(@user_creation).should eq ["enqueuing", "creating_user", "validating_user", "saving_user",
"promoting_user", "load_common_data", "success"]
end
it 'with Central as a viewer, skips loading common data' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
User.any_instance.stubs(:validate_credentials_not_taken_in_central).returns(true)
@user_creation.viewer = true
@user_creation.expects(:create_in_central).once
@user_creation.expects(:load_common_data).never
creation_steps(@user_creation).should eq ["enqueuing", "creating_user", "validating_user", "saving_user",
"promoting_user", "creating_user_in_central", "success"]
end
it 'without Central as a viewer, skips loading common data and creation in central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@user_creation.viewer = true
@user_creation.expects(:create_in_central).never
@user_creation.expects(:load_common_data).never
creation_steps(@user_creation).should eq ["enqueuing", "creating_user", "validating_user", "saving_user",
"promoting_user", "success"]
end
end
end
+26
View File
@@ -0,0 +1,26 @@
require_relative '../../spec_helper_min'
describe Carto::UserDBService do
include_context 'organization with users helper'
before(:all) do
@user = Carto::User.find(FactoryGirl.create(:valid_user).id)
end
after(:all) do
@user.destroy
end
describe '#public_user_roles' do
it 'should return public user for non-org users' do
expect(Carto::UserDBService.new(@user).public_user_roles).to eq [CartoDB::PUBLIC_DB_USER]
end
it 'should return public user and org public user for org users' do
expect(Carto::UserDBService.new(@carto_org_user_1).public_user_roles).to eq [
CartoDB::PUBLIC_DB_USER,
"cartodb_publicuser_#{@carto_org_user_1.id}"
]
end
end
end
@@ -0,0 +1,143 @@
require_relative '../../spec_helper_min'
describe Carto::UserMigrationImport do
it '#import throws an exception if import_metadata and dry are passed' do
import = Carto::UserMigrationImport.create(
exported_file: 'irrelevant_file',
json_file: "irrelevant_json_file",
database_host: 'database_host',
user_id: 'irrelevant_user_id',
organization_id: 'irrelevant_organization_id',
import_metadata: true,
dry: true
)
import.run_import.should eq false
import.state.should eq 'failure'
import.log.entries.should match(/Dry dry cannot be true while import_metadata is true/)
end
it 'fails saving a migration with dry and import_metadata' do
import = Carto::UserMigrationImport.create(
exported_file: 'irrelevant_file',
json_file: "irrelevant_json_file",
database_host: 'database_host',
user_id: 'irrelevant_user_id',
organization_id: 'irrelevant_organization_id',
import_metadata: true,
dry: true
)
expect { import.save! }.to(
raise_error(ActiveRecord::RecordInvalid, /dry cannot be true while import_metadata is true/)
)
end
describe '#import' do
before :each do
@org = FactoryGirl.create(:organization)
@org.whitelisted_email_domains = []
@org.save
@user = FactoryGirl.create(:valid_user)
@user.save
@user = Carto::User.find(@user.id)
@import = Carto::UserMigrationImport.create(
exported_file: 'irrelevant_file',
json_file: "irrelevant_json_file",
database_host: 'database_host',
user_id: @user.id,
organization_id: @org.id,
import_metadata: true,
dry: false
)
setup_mocks
end
it 'updates database host for imported user' do
should_import_metadata_for_user(@user)
@import.org_import = false
@import.organization_id = nil
should_update_database_host_for_users([@user])
@import.run_import
end
it 'updates database host for all users in org' do
users = create_and_add_users_to_organizaton
should_import_metadata_for_organization(@organization_mock)
@import.stubs(:update_attributes)
@import.org_import = true
@import.user_id = nil
should_update_database_host_for_users(users)
@import.run_import
end
private
def should_update_database_host_for_users(users)
sequel_user_mock = Object.new
::User.stubs(:[]).returns(sequel_user_mock)
sequel_user_mock.expects(:reload).times(users.length)
users.each do |user|
user.expects(:database_host=).with('database_host').once
user.expects(:save!).once
end
end
def setup_mocks
@organization_mock = Carto::Organization.new
@import.stubs(:assert_organization_does_not_exist)
@import.stubs(:assert_user_does_not_exist)
@user_migration_package_mock = Object.new
Carto::UserMigrationPackage.stubs(:for_import).returns @user_migration_package_mock
@user_migration_package_mock.stubs(:download).with('irrelevant_file')
@user_migration_package_mock.stubs(:meta_dir).returns('irrelevant_meta_dir')
@user_migration_package_mock.stubs(:data_dir).returns('irrelevant_data_dir')
@user_mock = Carto::User.new
@import_job_mock = Object.new
@import_job_mock.stubs(:db_exists?).returns false
@import_job_mock.expects(:run!).once
@import_job_mock.expects(:terminate_connections).once
CartoDB::DataMover::ImportJob.stubs(:new).returns @import_job_mock
@user_migration_package_mock.stubs(:cleanup)
@import.expects(:save!).once.returns @import
end
def expected_job_arguments
{
job_uuid: nil,
file: "irrelevant_data_dir/irrelevant_json_file",
data: true,
metadata: false,
host: :database_host,
rollback: false,
into_org_name: nil,
mode: :import,
logger: @import.log.logger,
import_job_logger: @import.log.logger
}
end
def should_import_metadata_for_user(user)
@user_migration_package_mock.stubs(:meta_dir).returns 'irrelevant_meta_dir'
Carto::UserMetadataExportService.any_instance.expects(:import_from_directory).with('irrelevant_meta_dir')
.returns user
Carto::UserMetadataExportService.any_instance.expects(:import_metadata_from_directory)
.with(user, 'irrelevant_meta_dir')
end
def should_import_metadata_for_organization(organization)
@user_migration_package_mock.stubs(:meta_dir).returns 'irrelevant_meta_dir'
Carto::OrganizationMetadataExportService.any_instance.stubs(:import_from_directory).with('irrelevant_meta_dir')
.once.returns organization
Carto::OrganizationMetadataExportService.any_instance.stubs(:import_metadata_from_directory)
.once.with(organization, 'irrelevant_meta_dir')
end
def create_and_add_users_to_organizaton
user2 = Carto::User.new
user3 = Carto::User.new
@organization_mock.users << user2
@organization_mock.users << user3
end
end
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,156 @@
require 'spec_helper_min'
describe Carto::UserMultifactorAuth do
def totp_code(mfa)
ROTP::TOTP.new(mfa.shared_secret).now
end
before :all do
@valid_type = 'totp'
@carto_user = FactoryGirl.create(:carto_user)
end
before :each do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(false)
@carto_user.reload.user_multifactor_auths.each(&:destroy!)
end
after :all do
@carto_user.destroy!
end
describe '#create' do
it 'does not validate an unsupported multi-factor authentication type' do
mfa = Carto::UserMultifactorAuth.new(user_id: @carto_user.id, type: 'wadus')
mfa.valid?.should eq(false)
end
it 'validates supported multi-factor authentication types' do
mfa = Carto::UserMultifactorAuth.new(user_id: @carto_user.id, type: @valid_type)
expect { mfa.valid? }.to be_true
end
it 'creates a new multifactor auth for a user' do
expect {
Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
}.to_not raise_error
end
it 'populates shared_secret' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
expect { mfa.shared_secret }.to_not be_nil
end
it 'is disabled by default' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
expect { mfa.disabled? }.to be_true
end
it 'syncs to central' do
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central
.any_instance
.expects(:update_user)
.with(@carto_user.username,
has_entries(multifactor_authentication_status: User::MULTIFACTOR_AUTHENTICATION_NEEDS_SETUP))
.once
Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
end
end
describe '#update' do
it 'syncs enabled to central' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central
.any_instance
.expects(:update_user)
.with(@carto_user.username,
has_entries(multifactor_authentication_status: User::MULTIFACTOR_AUTHENTICATION_ENABLED))
.once
mfa.enabled = true
mfa.save!
end
it 'syncs needs setup to central' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type, enabled: true)
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central
.any_instance
.expects(:update_user)
.with(@carto_user.username,
has_entries(multifactor_authentication_status: User::MULTIFACTOR_AUTHENTICATION_NEEDS_SETUP))
.once
mfa.enabled = false
mfa.save!
end
it 'syncs disabled to central' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
Cartodb::Central.stubs(:sync_data_with_cartodb_central?).returns(true)
Cartodb::Central
.any_instance
.expects(:update_user)
.with(@carto_user.username,
has_entries(multifactor_authentication_status: User::MULTIFACTOR_AUTHENTICATION_DISABLED))
.once
mfa.destroy!
end
end
describe '#verify!' do
it 'verifies a valid code' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
code = totp_code(mfa)
mfa.verify!(code)
expect { mfa.enabled }.to be_true
end
it 'does not allow reuse of a valid code' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
code = totp_code(mfa)
mfa.verify!(code)
expect { mfa.verify!(code) }.to raise_error("The code is not valid")
end
it 'does not verify an invalid code' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
totp = ROTP::TOTP.new(ROTP::Base32.random_base32)
expect { mfa.verify!(totp.now) }.to raise_error("The code is not valid")
end
it 'does not allow use of future/past tokens' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
totp = ROTP::TOTP.new(mfa.shared_secret)
expect { mfa.verify!(totp.at(Time.now - 90.seconds)) }.to raise_error("The code is not valid")
end
it 'verifies a code taking into account 30 seconds drift' do
mfa = Carto::UserMultifactorAuth.create!(user_id: @carto_user.id, type: @valid_type)
totp = ROTP::TOTP.new(mfa.shared_secret)
expect { mfa.verify!(totp.at(Time.now - 30.seconds)) }.to be_true
end
end
describe '#provisioning_uri' do
before :each do
@multifactor_auth = FactoryGirl.create(:totp, :active, user: @carto_user)
end
after :each do
@multifactor_auth.destroy!
end
it 'provides a provisioning_uri' do
uri = "otpauth://totp/CARTO:#{@carto_user.username}?secret=#{@multifactor_auth.shared_secret}&issuer=CARTO"
@multifactor_auth.provisioning_uri.should eq uri
end
end
end
+133
View File
@@ -0,0 +1,133 @@
require_relative '../../spec_helper'
describe Carto::UserService do
before(:all) do
@user = FactoryGirl.create(:valid_user)
end
before(:each) do
delete_user_data(@user)
$pool.close_connections!
end
after(:all) do
bypass_named_maps
@user.destroy
end
describe "#in_database" do
it "initializes the connection with the expected options" do
carto_user = Carto::User.find(@user.id)
default_opts = {
username: carto_user.database_username,
password: carto_user.database_password,
user_schema: carto_user.database_schema
}
expected_params = [carto_user.database_host, carto_user.database_name, default_opts]
Carto::Db::Connection.expects(:connect).with(*expected_params)
carto_user.in_database
end
it "sets statement timeout option" do
custom_timeout = 123456
expected_returned_custom_timeout = { statement_timeout: "#{custom_timeout}ms" }
@returned_timeout = nil
@default_timeout = nil
@returned_timeout_new = nil
@default_timeout_new = nil
@user.in_database do |db|
@default_timeout = db[%{SHOW statement_timeout}].first
end
@user.in_database(statement_timeout: custom_timeout) do |db|
@returned_timeout = db[%{SHOW statement_timeout}].first
end
@returned_timeout.should eq expected_returned_custom_timeout
@default_timeout.should_not eq @returned_timeout
@user.in_database do |db|
@default_timeout.should eq db[%{SHOW statement_timeout}].first
end
# Now test with CARTO user
carto_user = Carto::User.find(@user.id)
carto_user.in_database do |db|
@default_timeout_new = db.execute(%{SHOW statement_timeout}).first
end
carto_user.in_database(statement_timeout: custom_timeout) do |db|
@returned_timeout_new = db.execute(%{SHOW statement_timeout}).first
end
@returned_timeout_new.symbolize_keys!
@default_timeout_new .symbolize_keys!
@returned_timeout_new.should eq expected_returned_custom_timeout
@default_timeout_new.should_not eq @returned_timeout_new
carto_user.in_database do |db|
@default_timeout_new.should eq db.execute(%{SHOW statement_timeout}).first.symbolize_keys
end
@default_timeout_new .symbolize_keys!
@returned_timeout_new.should eq @returned_timeout
@default_timeout_new.should eq @default_timeout
end
it "sets search_path correctly" do
expected_returned_normal_search_path = {
search_path: "#{@user.database_schema}, cartodb, cdb_dataservices_client, public"
}
@normal_search_path = nil
@normal_search_path_new = nil
@user.in_database do |db|
@normal_search_path = db[%{SHOW search_path}].first
end
@normal_search_path.should eq expected_returned_normal_search_path
carto_user = Carto::User.find(@user.id)
carto_user.in_database do |db|
@normal_search_path_new = db.execute(%{SHOW search_path}).first
end
@normal_search_path_new.symbolize_keys!
@normal_search_path_new.should eq expected_returned_normal_search_path
@normal_search_path_new.should eq @normal_search_path
end
it "only allows superadmin operations to the expected roles" do
carto_user = Carto::User.find(@user.id)
expect {
@user.in_database do |conn|
conn.execute(%{SELECT set_config('log_statement_stats', 'off', false)})
end
}.to raise_exception(Sequel::DatabaseError, /permission denied to set parameter "log_statement_stats"/)
expect {
carto_user.in_database do |conn|
conn.execute(%{SELECT set_config('log_statement_stats', 'off', false)})
end
}.to raise_exception(ActiveRecord::StatementInvalid, /permission denied to set parameter "log_statement_stats"/)
@user.in_database(as: :superuser) do |conn|
conn.execute(%{SELECT set_config('log_statement_stats', 'off', false)})
end
carto_user.in_database(as: :superuser) do |conn|
conn.execute(%{SELECT set_config('log_statement_stats', 'off', false)})
end
@user.in_database(as: :cluster_admin) do |conn|
conn.execute(%{SELECT set_config('log_statement_stats', 'off', false)})
end
carto_user.in_database(as: :cluster_admin) do |conn|
conn.execute(%{SELECT set_config('log_statement_stats', 'off', false)})
end
end
end
end
+98
View File
@@ -0,0 +1,98 @@
require_relative '../../spec_helper'
require_relative '../user_shared_examples'
describe Carto::User do
it_behaves_like 'user models' do
def get_twitter_imports_count_by_user_id(user_id)
get_user_by_id(user_id).twitter_imports_count
end
def get_user_by_id(user_id)
Carto::User.where(id: user_id).first
end
def create_user
FactoryGirl.create(:carto_user)
end
end
describe '#needs_password_confirmation?' do
it 'is true for a normal user' do
user = FactoryGirl.build(:carto_user, google_sign_in: nil)
user.needs_password_confirmation?.should == true
user = FactoryGirl.build(:carto_user, google_sign_in: false)
user.needs_password_confirmation?.should == true
end
it 'is false for users that signed in with Google' do
user = FactoryGirl.build(:carto_user, google_sign_in: true)
user.needs_password_confirmation?.should == false
end
it 'is true for users that signed in with Google but changed the password' do
user = FactoryGirl.build(:carto_user, google_sign_in: true, last_password_change_date: Time.now)
user.needs_password_confirmation?.should == true
end
end
describe '#soft_geocoding_limit' do
before(:all) do
@carto_user = FactoryGirl.build(:carto_user)
end
it 'false for free accounts' do
@carto_user.account_type = 'FREE'
@carto_user.soft_geocoding_limit?.should be_false
end
it 'false for BASIC and PRO accounts' do
['BASIC', 'PRO', 'Individual'].each do |account_type|
@carto_user.account_type = account_type
@carto_user.soft_geocoding_limit?.should be_false
end
end
end
describe '#default_dataset_privacy' do
it 'returns the equivalent visualization privacy' do
no_private_tables_user = FactoryGirl.build(:carto_user, private_tables_enabled: false)
no_private_tables_user.default_dataset_privacy.should eq Carto::Visualization::PRIVACY_PUBLIC
private_tables_user = FactoryGirl.build(:carto_user, private_tables_enabled: true)
private_tables_user.default_dataset_privacy.should eq Carto::Visualization::PRIVACY_PRIVATE
end
end
describe "#send_password_reset!" do
before(:all) do
@user = FactoryGirl.create(:carto_user)
end
after(:all) do
@user.destroy
end
it 'enqueues a job to send an email' do
Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::PasswordReset, @user.id)
@user.send_password_reset!
end
it 'updates password_reset_token' do
expect { @user.send_password_reset! }.to change(@user, :password_reset_token)
end
it 'updates password_reset_sent_at' do
now = Time.zone.now
Delorean.time_travel_to(now) do
@user.send_password_reset!
end
@user.password_reset_sent_at.to_s.should eql now.to_s
end
end
end
+123
View File
@@ -0,0 +1,123 @@
require_relative '../../spec_helper_min'
require 'models/user_table_shared_examples'
describe Carto::UserTable do
include UniqueNamesHelper
before(:all) do
bypass_named_maps
@user = FactoryGirl.create(:carto_user)
@carto_user = @user
@user_table = Carto::UserTable.new
@user_table.user = @user
@user_table.name = unique_name('user_table')
@user_table.save
# The dependent visualization models are in the UserTable class for the AR model
@dependent_test_object = @user_table
end
after(:all) do
@user_table.destroy
@user.destroy
end
it_behaves_like 'user table models' do
def build_user_table(attrs = {})
ut = Carto::UserTable.new
ut.assign_attributes(attrs, without_protection: true)
ut
end
end
describe 'table_id column' do
it 'supports values larger than 2^31-1' do
column = Carto::UserTable.columns.find{|c| c.name=='table_id'}
expect { column.type_cast_for_database(2164557046) }.to_not raise_error
end
end
describe 'canonical visualization' do
it 'contains 1 data layer and creates a named map template if default basemap supports labels on top' do
Carto::LayerFactory.build_default_base_layer(@user).supports_labels_layer?.should be_true
Carto::NamedMaps::Api.any_instance.unstub(:show, :create, :update, :destroy)
Carto::NamedMaps::Api.any_instance.expects(:create).once
ut = Carto::UserTable.new
ut.user = @user
ut.save!
ut.visualization.data_layers.count.should eq 1
end
it 'contains 1 data layer and creates a named map template if default basemap does not support labels on top' do
old_google_maps_key = @user.google_maps_key
@user.update_attribute(:google_maps_key, 'wadus')
Carto::LayerFactory.build_default_base_layer(@user).supports_labels_layer?.should be_false
Carto::NamedMaps::Api.any_instance.unstub(:show, :create, :update, :destroy)
Carto::NamedMaps::Api.any_instance.expects(:create).once
ut = Carto::UserTable.new
ut.user = @user
ut.save!
ut.visualization.data_layers.count.should eq 1
@user.update_attribute(:google_maps_key, old_google_maps_key)
end
end
describe '#default_privacy' do
it 'sets privacy to nil by default' do
expect(Carto::UserTable.new.privacy).to be_nil
end
it 'lets caller specify privacy' do
[UserTable::PRIVACY_PRIVATE, UserTable::PRIVACY_LINK, UserTable::PRIVACY_PUBLIC].each do |privacy|
expect(Carto::UserTable.new(privacy: privacy).privacy).to eq privacy
end
end
end
describe '#readable_by?' do
include_context 'organization with users helper'
include TableSharing
it 'returns true for shared tables' do
@table = create_table(privacy: UserTable::PRIVACY_PRIVATE, name: "a_table_name", user_id: @org_user_1.id)
user_table = Carto::UserTable.find(@table.id)
share_table_with_user(@table, @org_user_2)
user_table.readable_by?(@carto_org_user_2).should be_true
end
end
describe('#affected_visualizations') do
before(:each) do
# We recreate an inconsistent state where a layer has no visualization
@user_table.stubs(:layers).returns([Carto::Layer.new])
end
describe('#fully_dependent_visualizations') do
it 'resists layers without visualizations' do
expect { @user_table.fully_dependent_visualizations }.to_not raise_error
end
end
describe('#accessible_dependent_derived_maps') do
it 'resists layers without visualizations' do
expect { @user_table.accessible_dependent_derived_maps }.to_not raise_error
end
end
describe('#partially_dependent_visualizations') do
it 'resists layers without visualizations' do
expect { @user_table.partially_dependent_visualizations }.to_not raise_error
end
end
end
end
@@ -0,0 +1,43 @@
require 'spec_helper_min'
module Carto
describe UsernameProposer do
describe '#find_unique' do
before(:all) do
@users = Array.new
@users << FactoryGirl.create(:carto_user, username: 'manolo')
@users << FactoryGirl.create(:carto_user, username: 'manolo-1')
end
after(:all) do
@users.map(&:destroy)
end
it 'returns same if available' do
UsernameProposer.find_unique('escobar').should eq 'escobar'
end
it 'returns unique if not available' do
UsernameProposer.find_unique('manolo').should eq 'manolo-2'
end
it 'fills in the gaps' do
@users << FactoryGirl.create(:carto_user, username: 'manolo-3')
UsernameProposer.find_unique('manolo').should eq 'manolo-2'
end
it 'returns same if max_retries is 0' do
UsernameProposer.find_unique('manolo', max_retries: 0).should eq 'manolo'
end
it 'starts at given offset' do
UsernameProposer.find_unique('manolo', offset: 8).should eq 'manolo-8'
end
it 'gives up if max_retries is reached' do
UsernameProposer.find_unique('manolo', max_retries: 1).should eq 'manolo-1'
end
end
end
end
@@ -0,0 +1,77 @@
require_relative '../../../spec_helper'
describe Carto::Visualization::Watcher do
before(:all) do
$tables_metadata.flushdb
end
after(:all) do
$tables_metadata.flushdb
end
describe '#basic_tests' do
it 'tests notify and list methods' do
watcher_ttl = 2
org_id = UUIDTools::UUID.timestamp_create.to_s
organization_mock = mock
organization_mock.stubs(:id).returns(org_id)
# Normal user
user1_name = 'wadus1'
user1_mock = mock
user1_mock.stubs(:username).returns(user1_name)
user1_mock.stubs(:organization).returns(organization_mock)
#Another org user
user2_name = 'wadus2'
user2_mock = mock
user2_mock.stubs(:username).returns(user2_name)
user2_mock.stubs(:organization).returns(organization_mock)
vis_id = UUIDTools::UUID.timestamp_create.to_s
visualization_mock = mock
visualization_mock.stubs(:id).returns(vis_id)
# Try to create a watcher from a non-org user
expect {
non_org_user_mock = mock
non_org_user_mock.stubs(:organization).returns(nil)
Carto::Visualization::Watcher.new(non_org_user_mock, visualization_mock)
}.to raise_exception Carto::Visualization::WatcherError
u1_watcher = Carto::Visualization::Watcher.new(user1_mock, visualization_mock, watcher_ttl)
u1_watcher.nil?.should eq false
u1_watcher.list.should eq []
u1_watcher.notify
u1_watcher.list.should eq [user1_mock.username]
sleep(watcher_ttl+1)
u1_watcher.list.should eq []
u1_watcher.notify
u1_watcher.list.should eq [user1_mock.username]
u2_watcher = Carto::Visualization::Watcher.new(user2_mock, visualization_mock, watcher_ttl)
u2_watcher.nil?.should eq false
# All watchers must notify all users
u2_watcher.list.should eq [user1_mock.username]
u2_watcher.notify
# Both watchers should be now updated
(u2_watcher.list - [user1_mock.username, user2_mock.username]).should eq []
(u1_watcher.list - [user1_mock.username, user2_mock.username]).should eq []
sleep(watcher_ttl+1)
# And empty
u1_watcher.list.should eq []
u2_watcher.list.should eq []
u2_watcher.notify
sleep(1)
u1_watcher.list.should eq [user2_mock.username]
u2_watcher.list.should eq [user2_mock.username]
end
end
end
@@ -0,0 +1,228 @@
require 'spec_helper'
require 'support/factories/users'
require 'helpers/named_maps_helper'
describe Carto::DataExporter do
include CartoDB::Factories
include NamedMapsHelper
describe '#export_tables' do
include_context 'user helper'
# INFO: this needs SQL API configured for testing
it 'exports a table in requested format' do
table_name = 'table1'
table = create_table(name: table_name, user_id: @user.id)
user_table = Carto::UserTable.find_by_table_id(table.get_table_id)
table.insert_row!(id: 666, description: 'desc', name: 'a row')
format = 'shp'
tmp_dir = "/tmp/export_test_#{String.random(15)}"
FileUtils.mkdir_p tmp_dir
begin
Carto::Http::Request.any_instance.stubs(:run)
file = File.new(Carto::DataExporter.new.export_table(user_table, tmp_dir, format))
file.path.should match(/.#{format}$/)
file.close
ensure
File.delete file if file
FileUtils.rm_rf tmp_dir
table.destroy
end
end
end
end
describe Carto::VisualizationExport do
include Carto::ExporterConfig
include_context 'users helper'
include Carto::Factories::Visualizations
class FakeCartoHttpClientFileToucher
attr_reader :touched_files
def initialize
@touched_files = []
end
# Mock needs not url nor Typhoeus options
def get_file(_, exported_file, _)
@touched_files << exported_file
touch(exported_file)
end
private
def random_filename(dir, extension: 'shp')
"#{dir}/test_#{String.random(12)}.#{extension}".downcase
end
def touch_random_filename(dir, extension: 'shp')
touch(random_filename(dir, extension: extension))
end
def touch(path)
FileUtils.touch(path).first
end
end
let(:base_dir) { ensure_clean_folder('/tmp/exporter_test') }
describe '#export' do
it 'exports a .carto file including the carto.json and the files' do
map = FactoryGirl.create(:carto_map, user: @carto_user1)
table1 = FactoryGirl.create(:private_user_table, user: @carto_user1)
table2 = FactoryGirl.create(:private_user_table, user: @carto_user1)
layer1 = FactoryGirl.create(:carto_layer, options: { table_name: table1.name }, maps: [map])
FactoryGirl.create(:carto_layer, options: { table_name: table2.name }, maps: [map])
map, table, table_visualization, visualization = create_full_visualization(@carto_user1,
map: map,
table: table1,
data_layer: layer1)
fake_carto_http_client_toucher = FakeCartoHttpClientFileToucher.new
exported_file = Carto::VisualizationExport.new.export(
visualization,
@carto_user1,
base_dir: base_dir,
data_exporter: Carto::DataExporter.new(fake_carto_http_client_toucher))
touched_files = fake_carto_http_client_toucher.touched_files
CartoDB::Importer2::Unp.new.open(exported_file) do |files|
files.length.should eq (map.layers.count + 1)
names = files.map(&:path)
names.count { |f| f =~ /\.carto\.json$/ }.should eq 1
names.should include(touched_files[0].split('/').last)
names.should include(touched_files[1].split('/').last)
end
([exported_file] + touched_files).map { |f| File.delete(f) if File.exists?(f) }
destroy_full_visualization(map, table, table_visualization, visualization)
end
it 'excludes data not accessible by the user' do
map = FactoryGirl.create(:carto_map, user: @carto_user1)
table1 = FactoryGirl.create(:private_user_table, user: @carto_user1)
layer1 = FactoryGirl.create(:carto_layer, options: { table_name: table1.name }, maps: [map])
map, table, table_visualization, visualization = create_full_visualization(@carto_user1,
map: map,
table: table1,
data_layer: layer1)
fake_carto_http_client_toucher = FakeCartoHttpClientFileToucher.new
exported_file = Carto::VisualizationExport.new.export(
visualization,
@carto_user2,
base_dir: base_dir,
data_exporter: Carto::DataExporter.new(fake_carto_http_client_toucher))
touched_files = fake_carto_http_client_toucher.touched_files
CartoDB::Importer2::Unp.new.open(exported_file) do |files|
files.length.should eq 1 # visualization export
names = files.map(&:path)
names.count { |f| f =~ /\.carto\.json$/ }.should eq 1
end
([exported_file] + touched_files).map { |f| File.delete(f) if File.exists?(f) }
destroy_full_visualization(map, table, table_visualization, visualization)
end
it 'excludes layers and user_tables with user_tables_ids parameter' do
map = FactoryGirl.create(:carto_map, user: @carto_user1)
table1 = FactoryGirl.create(:private_user_table, user: @carto_user1)
table2 = FactoryGirl.create(:private_user_table, user: @carto_user1)
layer1 = FactoryGirl.create(:carto_layer, options: { table_name: table1.name }, maps: [map])
FactoryGirl.create(:carto_layer, options: { table_name: table2.name }, maps: [map])
map, table, table_visualization, visualization = create_full_visualization(@carto_user1,
map: map,
table: table1,
data_layer: layer1)
fake_carto_http_client_toucher = FakeCartoHttpClientFileToucher.new
exported_file = Carto::VisualizationExport.new.export(
visualization,
@carto_user1,
user_tables_ids: [table1.id],
base_dir: base_dir,
data_exporter: Carto::DataExporter.new(fake_carto_http_client_toucher))
touched_files = fake_carto_http_client_toucher.touched_files
CartoDB::Importer2::Unp.new.open(exported_file) do |files|
files.length.should eq (1 + 1) # selected user_table + metadata
names = files.map(&:path)
names.count { |f| f =~ /\.carto\.json$/ }.should eq 1
names.should include(touched_files[0].split('/').last)
touched_files.length.should eq 1
end
([exported_file] + touched_files).map { |f| File.delete(f) if File.exists?(f) }
destroy_full_visualization(map, table, table_visualization, visualization)
end
end
describe '#run_export!' do
# run_export! is called from the job, but the core of the logic is in `#export`, this makes sure its usage
it 'calls #export' do
visualization = FactoryGirl.create(:carto_visualization, user: @carto_user1)
ve = FactoryGirl.create(:visualization_export, user: @carto_user1, visualization: visualization)
fake_path = "/tmp/fakepath"
touch(fake_path)
ve.expects(:export).returns(fake_path)
file_upload_helper_mock = mock
file_upload_helper_mock.expects(:upload_file_to_storage)
ve.run_export!(file_upload_helper: file_upload_helper_mock)
visualization.destroy
File.delete(fake_path) if File.exists?(fake_path)
end
describe 'with S3' do
before(:each) do
@visualization = FactoryGirl.create(:carto_visualization, user: @carto_user1)
end
after(:each) do
@visualization.destroy
end
let(:test_dir) { '/tmp/exports_test/' }
let(:fake_path) { "#{test_dir}/fake_export.carto" }
it 'runs export creating a log trace' do
ve = Carto::VisualizationExport.new(visualization: @visualization, user: @carto_user1)
FileUtils.mkdir_p test_dir
FileUtils.touch(fake_path).first
ve.expects(:export).returns(fake_path)
file_upload_helper_mock = mock
file_upload_helper_mock.expects(:upload_file_to_storage).returns(file_url: fake_path)
ve.run_export!(file_upload_helper: file_upload_helper_mock)
ve.reload
ve.log.should_not be_nil
ve.destroy
end
end
end
end
+659
View File
@@ -0,0 +1,659 @@
require_relative '../../spec_helper'
require_relative '../visualization_shared_examples'
require_relative '../../../app/models/visualization/member'
require_relative '../../support/factories/organizations'
require 'helpers/unique_names_helper'
require 'helpers/visualization_destruction_helper'
describe Carto::Visualization do
include UniqueNamesHelper
include VisualizationDestructionHelper
include Carto::Factories::Visualizations
before(:all) do
@user = create_user
@carto_user = Carto::User.find(@user.id)
@user2 = create_user
@carto_user2 = Carto::User.find(@user2.id)
end
before(:each) do
bypass_named_maps
delete_user_data(@user)
end
after(:all) do
bypass_named_maps
@user.destroy
@user2.destroy
end
it_behaves_like 'visualization models' do
def build_visualization(attrs = {})
v = Carto::Visualization.new
v.assign_attributes(attrs, without_protection: true)
v
end
end
describe '#valid?' do
it 'validates visualization member with proper type' do
map = ::Map.create(user_id: @user.id)
CartoDB::Visualization::Member::VALID_TYPES.each do |type|
parent = nil
if type == CartoDB::Visualization::Member::TYPE_SLIDE
parent = CartoDB::Visualization::Member.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: map.id,
type: CartoDB::Visualization::Member::TYPE_DERIVED,
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC
).store
end
viz = CartoDB::Visualization::Member.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: map.id,
type: type,
parent_id: parent.try(:id),
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC
)
viz.valid?.should be_true
end
end
it 'validates carto visualization with proper type' do
map = ::Map.create(user_id: @user.id)
Carto::Visualization::VALID_TYPES.each do |type|
viz = Carto::Visualization.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: map.id,
type: type,
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC
)
viz.valid?.should be_true
end
end
it 'throws error if not valid type' do
map = ::Map.create(user_id: @user.id)
viz = CartoDB::Visualization::Member.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: map.id,
type: 'whatever',
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC
)
viz.valid?.should be_false
expect {
viz.store
}.to raise_error(CartoDB::InvalidMember, /Visualization type is not valid/)
end
it 'throws error if not valid type using carto model' do
map = ::Map.create(user_id: @user.id)
viz = Carto::Visualization.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: map.id,
type: 'whatever',
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC
)
viz.valid?.should be_false
expect {
viz.save!
}.to raise_error(ActiveRecord::RecordInvalid, /Type is not included in the list/)
end
end
describe '#estimated_row_count and #actual_row_count' do
it 'should query Table estimated an actual row count methods' do
::Table.any_instance.stubs(:row_count_and_size).returns(row_count: 999)
::Table.any_instance.stubs(:actual_row_count).returns(1000)
table = create_table(name: 'table1', user_id: @user.id)
vis = Carto::Visualization.find(table.table_visualization.id)
vis.estimated_row_count.should == 999
vis.actual_row_count.should == 1000
end
end
describe '#tags=' do
it 'should not set blank tags' do
vis = Carto::Visualization.new
vis.tags = ["tag1", " ", ""]
vis.tags.should eq ["tag1"]
end
end
describe '#privacy=' do
it 'downcases privacy' do
visualization = Carto::Visualization.new
visualization.privacy = Carto::Visualization::PRIVACY_LINK.upcase
visualization.privacy.should eq Carto::Visualization::PRIVACY_LINK.downcase
end
end
describe 'children' do
it 'should correctly count children' do
map = ::Map.create(user_id: @user.id)
parent = CartoDB::Visualization::Member.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: map.id,
type: CartoDB::Visualization::Member::TYPE_DERIVED,
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC
).store
child = CartoDB::Visualization::Member.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: ::Map.create(user_id: @user.id).id,
type: Visualization::Member::TYPE_SLIDE,
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC,
parent_id: parent.id
).store
parent = Carto::Visualization.where(id: parent.id).first
parent.children.count.should == 1
child2 = CartoDB::Visualization::Member.new(
user_id: @user.id,
name: unique_name('viz'),
map_id: ::Map.create(user_id: @user.id).id,
type: Visualization::Member::TYPE_SLIDE,
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC,
parent_id: parent.id
).store
child.set_next_list_item!(child2)
parent = Carto::Visualization.where(id: parent.id).first
parent.children.count.should == 2
end
end
describe 'licenses' do
it 'should store correctly a visualization with its license' do
table = create_table(name: 'table1', user_id: @user.id)
v = table.table_visualization
v.license = Carto::License::APACHE_LICENSE
v.store
vis = Carto::Visualization.find(v.id)
vis.license_info.id.should eq :apache
vis.license_info.name.should eq "Apache license"
end
end
describe '#related_tables_readable_by' do
include Carto::Factories::Visualizations
it 'only returns tables that a user can read' do
@carto_user.update_attribute(:private_tables_enabled, true)
map = FactoryGirl.create(:carto_map, user: @carto_user)
private_table = FactoryGirl.create(:private_user_table, user: @carto_user)
public_table = FactoryGirl.create(:public_user_table, user: @carto_user)
private_layer = FactoryGirl.create(:carto_layer, options: { table_name: private_table.name }, maps: [map])
FactoryGirl.create(:carto_layer, options: { table_name: public_table.name }, maps: [map])
map, table, table_visualization, visualization = create_full_visualization(@carto_user,
map: map,
table: private_table,
data_layer: private_layer)
related_table_ids_readable_by_owner = visualization.related_tables_readable_by(@carto_user).map(&:id)
related_table_ids_readable_by_owner.should include(private_table.id)
related_table_ids_readable_by_owner.should include(public_table.id)
related_table_ids_readable_by_others = visualization.related_tables_readable_by(@carto_user2).map(&:id)
related_table_ids_readable_by_others.should_not include(private_table.id)
related_table_ids_readable_by_others.should include(public_table.id)
destroy_full_visualization(map, table, table_visualization, visualization)
end
end
describe '#published?' do
before(:each) do
@visualization = FactoryGirl.build(:carto_visualization, user: @carto_user)
end
it 'returns true for visualizations without version' do
@visualization.version = nil
@visualization.published?.should eq true
end
it 'returns true for v2 visualizations' do
@visualization.version = 2
@visualization.published?.should eq true
end
it 'returns false for v3 visualizations' do
@visualization.version = 3
@visualization.published?.should eq false
end
it 'returns true for mapcapped v3 visualizations' do
@visualization.version = 3
@visualization.stubs(:mapcapped?).returns(true)
@visualization.published?.should eq true
end
end
describe '#can_be_private?' do
before(:all) do
bypass_named_maps
@visualization = FactoryGirl.create(:carto_visualization, user: @carto_user)
@visualization.reload # to clean up the user relation (see #11134)
end
after(:all) do
@visualization.destroy
end
it 'returns private_tables_enabled for tables' do
@visualization.type = 'table'
@visualization.can_be_private?.should eq @carto_user.private_tables_enabled
end
it 'returns private_maps_enabled for maps' do
@visualization.type = 'derived'
@visualization.can_be_private?.should eq @carto_user.private_maps_enabled
end
end
describe '#save_named_map' do
it 'should not save named map without layers' do
@visualization = FactoryGirl.build(:carto_visualization, user: @carto_user)
@visualization.expects(:named_maps_api).never
@visualization.save
end
it 'should save named map with layers on map creation' do
@visualization = FactoryGirl.build(:carto_visualization, user: @carto_user, map: FactoryGirl.build(:carto_map))
@visualization.layers << FactoryGirl.build(:carto_layer)
Carto::VisualizationInvalidationService.any_instance.expects(:invalidate).once
@visualization.save
end
describe 'without mapcap' do
before(:all) do
@map, @table, @table_visualization, @visualization = create_full_visualization(@carto_user2)
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
end
it 'publishes layer style changes' do
fake_style = 'this_is_a_very_fake_cartocss'
layer = @visualization.data_layers.first
layer.options[:tile_style] = fake_style
named_maps_api_mock = mock
named_maps_api_mock.expects(:upsert)
Carto::NamedMaps::Api.expects(:new).with { |v| v.data_layers.first.options[:tile_style] == fake_style }
.returns(named_maps_api_mock).at_least_once
layer.save
end
it 'publishes privacy changes' do
@visualization.privacy = Carto::Visualization::PRIVACY_PUBLIC
named_maps_api_mock = mock
named_maps_api_mock.expects(:upsert)
Carto::NamedMaps::Api.expects(:new).returns(named_maps_api_mock).at_least_once
@visualization.save
end
end
describe 'with mapcap' do
before(:all) do
@map, @table, @table_visualization, @visualization = create_full_visualization(@carto_user2)
@visualization.create_mapcap!
@visualization.reload
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
end
it 'does not publish layer style changes' do
fake_style = 'this_is_a_very_fake_cartocss'
layer = @visualization.data_layers.first
layer.options[:tile_style] = fake_style
named_maps_api_mock = mock
named_maps_api_mock.stubs(upsert: true)
Carto::NamedMaps::Api.stubs(:new).with { |v| v.data_layers.first.options[:tile_style] != fake_style }
.returns(named_maps_api_mock)
Carto::NamedMaps::Api.expects(:new).with { |v| v.data_layers.first.options[:tile_style] == fake_style }
.never
layer.save
end
it 'publishes layer style changes after mapcapping' do
fake_style = 'changed_style_again'
layer = @visualization.data_layers.first
layer.options[:tile_style] = fake_style
layer.save
named_maps_api_mock = mock
named_maps_api_mock.expects(:upsert).at_least_once
Carto::NamedMaps::Api.expects(:new).with { |v| v.data_layers.first.options[:tile_style] == fake_style }
.returns(named_maps_api_mock).at_least_once
@visualization.create_mapcap!
end
it 'publishes privacy changes' do
@visualization.privacy = Carto::Visualization::PRIVACY_PUBLIC
named_maps_api_mock = mock
named_maps_api_mock.expects(:upsert)
Carto::NamedMaps::Api.expects(:new).returns(named_maps_api_mock).at_least_once
@visualization.save
end
end
end
describe 'creation' do
it 'is not valid if user is viewer' do
viewer = FactoryGirl.build(:carto_user, viewer: true)
visualization = FactoryGirl.build(:carto_visualization, user: viewer)
visualization.valid?.should be_false
visualization.errors[:user].should_not be_empty
visualization.errors[:user].first.should eq "cannot be viewer"
end
end
describe '#destroy' do
it 'destroys all visualization dependencies' do
map = FactoryGirl.create(:carto_map_with_layers, user: @carto_user)
visualization = FactoryGirl.create(:carto_visualization, user: @carto_user, map: map)
FactoryGirl.create(:widget, layer: visualization.data_layers.first)
FactoryGirl.create(:analysis, visualization: visualization, user: @carto_user)
FactoryGirl.create(:carto_search_overlay, visualization: visualization)
FactoryGirl.create(:carto_synchronization, visualization: visualization)
visualization.create_mapcap!
visualization.state.save
Carto::VisualizationInvalidationService.any_instance.expects(:invalidate).once
expect_visualization_to_be_destroyed(visualization) { visualization.destroy }
end
end
describe '#backup' do
before(:all) do
@map = FactoryGirl.create(:carto_map_with_layers, user: @carto_user)
Carto::VisualizationBackup.all.map(&:destroy)
end
after(:all) do
@map.destroy
Carto::VisualizationBackup.all.map(&:destroy)
end
it 'creates a backup when visualization is destroyed' do
visualization = FactoryGirl.create(:carto_visualization, user: @carto_user, map: @map)
visualization.destroy
Carto::VisualizationBackup.all.count.should eq 1
backup = Carto::VisualizationBackup.where(visualization_id: visualization.id).first
backup.should_not eq nil
backup.user_id.should eq @carto_user.id
backup.created_at.should_not eq nil
backup.category.should eq Carto::VisualizationBackup::CATEGORY_VISUALIZATION
backup.export.should_not be_empty
backup.destroy
end
end
describe '#update' do
before(:all) do
@map, @table, @table_visualization, @visualization = create_full_visualization(@carto_user2)
@visualization.create_mapcap!
@visualization.reload
end
after(:all) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
end
it 'sanitizes name on rename' do
original = @table_visualization.name
@table_visualization.name = @table_visualization.name.upcase
@table_visualization.save!
@table_visualization.reload
expect(@table_visualization.name).to eq(original)
end
end
describe '#invalidation_service' do
before(:all) do
@visualization = FactoryGirl.create(:carto_visualization, user: @carto_user, type: 'table')
end
it 'triggers invalidation after saving' do
@visualization.send(:invalidation_service).expects(:invalidate)
@visualization.update_attributes(name: @visualization.name + '-renamed')
end
it 'triggers invalidation of affected maps after updating description field' do
# Not a great test, it tests invalidation service internal implementation
@visualization.send(:invalidation_service).expects(:invalidate_affected_visualizations)
@visualization.update_attributes(description: 'something')
end
it 'triggers invalidation of affected maps after updating attributions field' do
@visualization.send(:invalidation_service).expects(:invalidate_affected_visualizations)
@visualization.update_attributes(attributions: 'something')
end
it 'triggers invalidation after destroying' do
@visualization.send(:invalidation_service).expects(:invalidate)
@visualization.destroy
end
end
context 'like actions' do
before(:each) do
@visualization = FactoryGirl.create(:carto_visualization, user: @carto_user, type: 'table')
end
describe '#add_like_from' do
include_context 'organization with users helper'
it 'registers the like action from a user with permissions' do
expect(@visualization.likes.count).to eq(0)
@visualization.add_like_from(@carto_user)
expect(@visualization.likes.count).to eq(1)
expect(@visualization.liked_by?(@carto_user)).to be_true
end
it 'fail if a user try to favorite a visualization without permissions' do
user_id = UUIDTools::UUID.timestamp_create.to_s
user_mock = mock
user_mock.stubs(:id).returns(user_id)
expect(@visualization.likes.count).to eq(0)
expect {
@visualization.add_like_from(user_mock)
}.to raise_error Carto::Visualization::UnauthorizedLikeError
end
it 'raises an error if same user tries to like again the same content' do
user_id = UUIDTools::UUID.timestamp_create.to_s
user_mock = mock
user_mock.stubs(:id).returns(user_id)
@visualization.add_like_from(@carto_user)
expect(@visualization.likes.count).to eq(1)
expect(@visualization.liked_by?(@carto_user)).to be_true
expect {
@visualization.add_like_from(@carto_user)
}.to raise_error Carto::Visualization::AlreadyLikedError
expect(@visualization.likes.count).to eq(1)
end
it 'can add like to a shared visualization' do
visualization = FactoryGirl.build(:carto_visualization,
user: @carto_org_user_1,
privacy: Carto::Visualization::PRIVACY_LINK.upcase)
shared_entity = CartoDB::SharedEntity.new(
recipient_id: @carto_org_user_2.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: visualization.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
)
shared_entity.save
visualization.permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @carto_org_user_2.id,
username: @carto_org_user_2.username
},
access: Permission::ACCESS_READONLY
}
]
visualization.permission.save
visualization.add_like_from(@carto_org_user_2)
expect(visualization.likes.count).to eq(1)
end
end
describe '#remove_like_from' do
include_context 'organization with users helper'
it 'removes an existent like from a user' do
@visualization.add_like_from(@carto_user)
expect(@visualization.likes.count).to eq(1)
@visualization.remove_like_from(@carto_user)
expect(@visualization.likes.count).to eq(0)
expect(@visualization.liked_by?(@carto_user)).to be_false
end
it 'raises an error if you try to remove a favorite in a visualization you dont have permission' do
user_id = UUIDTools::UUID.timestamp_create.to_s
user_mock = mock
user_mock.stubs(:id).returns(user_id)
@visualization.add_like_from(@carto_user)
expect(@visualization.likes.count).to eq(1)
expect {
@visualization.remove_like_from(user_mock)
}.to raise_error Carto::Visualization::UnauthorizedLikeError
expect(@visualization.likes.count).to eq(1)
end
it 'can remove like from a shared visualization' do
visualization = FactoryGirl.build(:carto_visualization,
user: @carto_org_user_1,
privacy: Carto::Visualization::PRIVACY_LINK.upcase)
shared_entity = CartoDB::SharedEntity.new(
recipient_id: @carto_org_user_2.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: visualization.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
)
shared_entity.save
visualization.permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @carto_org_user_2.id,
username: @carto_org_user_2.username
},
access: Permission::ACCESS_READONLY
}
]
visualization.permission.save
visualization.add_like_from(@carto_org_user_2)
expect(visualization.likes.count).to eq(1)
visualization.remove_like_from(@carto_org_user_2)
expect(visualization.likes.count).to eq(0)
end
end
describe '#liked_by?' do
it 'returns true when the user liked the visualization' do
@visualization.add_like_from(@carto_user)
expect(@visualization.liked_by?(@carto_user)).to be_true
end
it 'returns false when checking a user without likes on the visualization' do
user_id = UUIDTools::UUID.timestamp_create.to_s
user_mock = mock
user_mock.stubs(:id).returns(user_id)
@visualization.add_like_from(@carto_user)
expect(@visualization.liked_by?(user_mock)).to be_false
end
end
end
context 'quota check' do
before(:each) do
@carto_user.private_maps_enabled = true
@carto_user.save
@visualization = FactoryGirl.create(:carto_visualization, user: @carto_user,
privacy: Carto::Visualization::PRIVACY_PRIVATE)
end
after(:all) do
@carto_user.public_map_quota = nil
@carto_user.save
end
it 'does not allow to make a map public when the limit is reached' do
@carto_user.public_map_quota = 0
@carto_user.save
@visualization.privacy = Carto::Visualization::PRIVACY_PUBLIC
@visualization.save
@visualization.reload.privacy.should eql Carto::Visualization::PRIVACY_PRIVATE
@visualization.errors.count.should eql 1
end
it 'allows to make the map public if the limit is not reached' do
@carto_user.public_map_quota = 1
@carto_user.save
@visualization.privacy = Carto::Visualization::PRIVACY_PUBLIC
@visualization.save
@visualization.reload.privacy.should eql Carto::Visualization::PRIVACY_PUBLIC
end
end
end
+148
View File
@@ -0,0 +1,148 @@
require 'json'
require_relative '../../spec_helper'
describe Carto::Widget do
describe 'Storage' do
it 'can be stored and retrieved keeping the data' do
widget = FactoryGirl.create(:widget_with_layer)
loaded_widget = Carto::Widget.find(widget.id)
loaded_widget.order.should == widget.order
loaded_widget.type.should == widget.type
loaded_widget.title.should == widget.title
loaded_widget.layer.should == widget.layer
loaded_widget.options.should == widget.options
loaded_widget.style.should == widget.style
widget.destroy
end
it 'is deleted if layer is deleted' do
widget = FactoryGirl.create(:widget_with_layer)
widget.layer.destroy
Carto::Widget.where(id: widget.id).first.should be_nil
end
describe '#save' do
before(:each) do
@user = FactoryGirl.create(:carto_user)
@map = FactoryGirl.create(:carto_map_with_layers, user: @user)
@widget = FactoryGirl.create(:widget, layer: @map.data_layers.first)
end
after(:each) do
@widget.destroy
@map.destroy
@user.destroy
end
it 'triggers notify_map_change on related map(s)' do
map = mock
map.stubs(:id).returns(@map.id)
map.expects(:notify_map_change).twice
Map.stubs(:where).with(id: map.id).returns([map])
@widget.title = "xxx#{@widget.title}"
@widget.save
end
end
end
describe 'Validations' do
describe 'Format and validation' do
before(:each) do
@widget = FactoryGirl.build(:widget_with_layer, options: { valid: 'format' })
end
it 'validates correct options format' do
@widget.valid?.should be_true
@widget.errors[:options].empty?.should be_true
end
it 'validates incorrect options format' do
@widget.options = 'badformat'
@widget.valid?.should be_false
@widget.errors[:options].empty?.should be_false
end
end
describe 'Source id validation' do
before(:each) do
@widget = FactoryGirl.build(:widget_with_layer, source_id: 'foo')
end
it 'accepts source id if cames as string' do
expect(@widget.valid?).to be_true
expect(@widget.errors[:source_id]).to eq([])
end
it 'rejects source id if cames as a hash' do
@widget.source_id = { foo: 'bar' }
expect(@widget.valid?).to be_false
expect(@widget.errors[:source_id]).to include('Source id must be a string')
end
end
end
describe '#from_visualization_id' do
before(:all) do
@user = FactoryGirl.create(:carto_user)
@map = FactoryGirl.create(:carto_map_with_layers, user: @user)
@visualization = FactoryGirl.create(:carto_visualization, map: @map)
end
after(:all) do
@visualization.destroy
@map.destroy
@user.destroy
end
it 'retrieves all visualization widgets' do
# Twice expectation: creation + destroy
Map.any_instance.expects(:update_related_named_maps).times(2).returns(true)
layer = @visualization.data_layers.first
widget = FactoryGirl.create(:widget, layer: layer)
widget2 = FactoryGirl.create(:widget_with_layer)
widgets = Carto::Widget.from_visualization_id(@visualization.id)
widgets.length.should == 1
widgets.should include(widget)
widgets.should_not include(widget2)
widget2.destroy
widget.destroy
end
end
context 'viewer users' do
before(:all) do
Map.any_instance.stubs(:update_related_named_maps)
@user = FactoryGirl.create(:carto_user)
@map = FactoryGirl.create(:carto_map_with_layers, user: @user)
@visualization = FactoryGirl.create(:carto_visualization, map: @map, user: @user)
@layer = @visualization.data_layers.first
@widget = FactoryGirl.create(:widget, layer: @layer)
@user.update_attribute(:viewer, true)
@layer.user.reload
end
after(:all) do
@visualization.destroy
@map.destroy
@user.destroy
end
it "can't create a new widget" do
widget = FactoryGirl.build(:widget, layer: @layer)
widget.save.should be_false
widget.errors[:layer].should eq(["Viewer users can't edit widgets"])
end
it "can't delete widgets" do
@widget.destroy.should eq false
Carto::Widget.exists?(@widget.id).should eq true
@widget.errors[:layer].should eq(["Viewer users can't edit widgets"])
end
end
end
+905
View File
@@ -0,0 +1,905 @@
require 'json'
require_relative '../spec_helper'
describe CommonData do
before(:each) do
Typhoeus::Expectation.clear
@common_data = CommonData.new('http://common-data.example.com/api/v1/viz?type=table&privacy=public')
@common_data.stubs(:config).with('protocol', 'https').returns('https')
@common_data.stubs(:config).with('username').returns('common-data')
@common_data.stubs(:config).with('base_url').returns(nil)
@common_data.stubs(:config).with('api_key').returns('wadus')
@common_data.stubs(:config).with('format', 'gpkg').returns('gpkg')
CommonDataRedisCache.new.invalidate
end
after(:all) do
Typhoeus::Expectation.clear
end
it 'should return empty datasets response and notify error for SQL API error response' do
stub_api_response(503)
CartoDB::Logger.expects(:error).with(message: 'common-data empty',
url: 'http://common-data.example.com/api/v1/viz?type=table&privacy=public')
@common_data.datasets.should eq []
end
it 'should return empty datasets and notify error for invalid json' do
stub_api_response(200, INVALID_JSON_RESPONSE)
count = 0
CartoDB::Logger.expects(:error).twice.with do |args|
if args[:exception]
args[:exception].should be_an_instance_of JSON::ParserError
count += 1
else
args[:message].should eq 'common-data empty'
args[:url].should eq 'http://common-data.example.com/api/v1/viz?type=table&privacy=public'
count -= 1
end
end
count.should eq 0 # Error called twice, one with each argument set
@common_data.datasets.should eq []
end
it 'should return correct datasets for default stub response' do
stub_valid_api_response
CartoDB::Logger.expects(:error).times(0)
@common_data.datasets.select{ |d| d["name"] =~ /meta_/}.length.should eq 0
@common_data.datasets.length.should eq 6
end
it 'should use name if the display_name is null' do
stub_valid_api_response
CartoDB::Logger.expects(:error).times(0)
@common_data.datasets.first['display_name'].should eq @common_data.datasets.first['name']
end
it 'reads the source and attributions' do
stub_valid_api_response
@common_data.datasets.first['source'].should eq '[Los Angeles opendata](http://datos.losangeles.com/)'
@common_data.datasets.first['attributions'].should eq 'CartoDB Inc.'
end
it 'categories should be an array' do
stub_valid_api_response
CartoDB::Logger.expects(:error).times(0)
(@common_data.datasets.first['tags'].is_a? Array).should eq true
end
it 'should use SQL API V2 for export URLs' do
stub_valid_api_response
@common_data.datasets.first['url'].should match (/common-data\.localhost\.lan\/api\/v2/)
end
it 'should use SQL API V2 from user defined base url for export URLs' do
@common_data.stubs(:config).with('base_url').returns("https://www.userdefinedurl.com")
stub_valid_api_response
@common_data.datasets.first['url'].should match (/^https:\/\/www\.userdefinedurl\.com\/api\/v2/)
end
def stub_valid_api_response
stub_api_response(200, VALID_JSON_RESPONSE)
end
def stub_api_response(code, body=nil)
if body
response = Typhoeus::Response.new(code: code, body: body)
else
response = Typhoeus::Response.new(code: code)
end
Typhoeus.stub(/common-data/).and_return(response)
end
VALID_JSON_RESPONSE = <<-response
{
"visualizations": [
{
"id": "c740998c-41ea-11e5-8cda-080027880ca6",
"name": "neighborhood_councils_losangeles",
"display_name": null,
"map_id": "2dcb5dde-85f1-4a9a-8e15-1ae71a1b6b8c",
"active_layer_id": "072d55ae-4d63-4a81-a44f-6ed158b47eaa",
"type": "table",
"tags": [
"Administrative regions"
],
"description": "Neighborhood Councils Los Angeles",
"privacy": "PUBLIC",
"stats": {
"2015-07-20": 0,
"2015-07-21": 0,
"2015-07-22": 0,
"2015-07-23": 0,
"2015-07-24": 0,
"2015-07-25": 0,
"2015-07-26": 0,
"2015-07-27": 0,
"2015-07-28": 0,
"2015-07-29": 0,
"2015-07-30": 0,
"2015-07-31": 0,
"2015-08-01": 0,
"2015-08-02": 0,
"2015-08-03": 0,
"2015-08-04": 0,
"2015-08-05": 0,
"2015-08-06": 0,
"2015-08-07": 0,
"2015-08-08": 0,
"2015-08-09": 0,
"2015-08-10": 0,
"2015-08-11": 0,
"2015-08-12": 0,
"2015-08-13": 0,
"2015-08-14": 0,
"2015-08-15": 0,
"2015-08-16": 0,
"2015-08-17": 0,
"2015-08-18": 0
},
"created_at": "2015-08-13T18:40:32+00:00",
"updated_at": "2015-08-18T08:15:13+00:00",
"permission": {
"id": "68ca2e6d-a7e9-4daf-aae1-908a9af31f5a",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "c740998c-41ea-11e5-8cda-080027880ca6",
"t ype": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:40:32+00:00",
"updated_at": "2015-08-13T18:40:32+00:00"
},
"locked": false,
"source": "[Los Angeles opendata](http://datos.losangeles.com/)",
"title": null,
"parent_id": null,
"license": "apache",
"attributions": "CartoDB Inc.",
"kind": "geom",
"likes": 0,
"prev_id": null,
"next_id": null,
"transition_options": {
"time": 0
},
"active_child": null,
"table": {
"id": "fa61b399-2de0-45c8-8008-cd0d527d40f3",
"name": "public.neighborhood_counci ls_losangeles",
"permission": {
"id": "68ca2e6d-a7e9-4daf-aae1-908a9af31f5a",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars /avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "c740998c-41ea-11e5-8cda-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:40:32+00:00",
"updated_at": "2015-08-13T18:40:32+00:00"
},
"geometry_types": [
"ST_MultiPolygon"
],
"privacy": "PUBLIC",
"updated_at": "2015-08-13T18:40:38+00:00",
"size": 1179648,
"row_count": 95
},
"external_source": {},
"sy nchronization": null,
"children": [],
"liked": false
},
{
"id": "b52a6246-41ea-11e5-b36d-080027880ca6",
"name": "districts_tajikistan",
"display_name": "Districts Tajikistan",
"map_id": "e53c662a-a838-4f0f-a938-56d4bc36681f",
"active_layer_i d": "f5c44bcc-af54-4d63-b916-2c06d52e0136",
"type": "table",
"tags": [
"Administrative regions"
],
"description": "Geometries for the districts (nohiya) in Tajikistan.",
"privacy": "PUBLIC",
"stats": {
"2015-07-20": 0,
"2015-07-21": 0,
"2015 -07-22": 0,
"2015-07-23": 0,
"2015-07-24": 0,
"2015-07-25": 0,
"2015-07-26": 0,
"2015-07-27": 0,
"2015-07-28": 0,
"2015-07-29": 0,
"2015-07-30": 0,
"2015-07-31": 0,
"2015-08-01": 0,
"2015-08-02": 0,
"2015-08-03": 0,
"2015-08-04": 0,
"2015 -08-05": 0,
"2015-08-06": 0,
"2015-08-07": 0,
"2015-08-08": 0,
"2015-08-09": 0,
"2015-08-10": 0,
"2015-08-11": 0,
"2015-08-12": 0,
"2015-08-13": 0,
"2015-08-14": 0,
"2015-08-15": 0,
"2015-08-16": 0,
"2015-08-17": 0,
"2015-08-18": 0
},
"cre ated_at": "2015-08-13T18:40:02+00:00",
"updated_at": "2015-08-18T08:15:13+00:00",
"permission": {
"id": "16e41a99-a441-4f2d-9561-184c320d9c5a",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "ma rio.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "b52a6246-41ea -11e5-b36d-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:40:01+00:00",
"updated_at": "2015-08-13T18:40:02+00:00"
},
"locked": false,
"source": "[Tajikistan Opendata](http://datos.tajikistan.com/)",
"title": null,
"p arent_id": null,
"license": "mit",
"attributions": null,
"kind": "geom",
"likes": 0,
"prev_id": null,
"next_id": null,
"transition_options": {
"time": 0
},
"active_child": null,
"table": {
"id": "5f3e3dd5-fb07-4e9a-8e06-2f4238cd9e1a",
"name": "pu blic.districts_tajikistan",
"permission": {
"id": "16e41a99-a441-4f2d-9561-184c320d9c5a",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example .com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "b52a6246-41ea-11e5-b36d-080027880ca6",
"type": "vis"
},
"acl": [],
" created_at": "2015-08-13T18:40:01+00:00",
"updated_at": "2015-08-13T18:40:02+00:00"
},
"geometry_types": [
"ST_MultiPolygon"
],
"privacy": "PUBLIC",
"updated_at": "2015-08-13T18:40:09+00:00",
"size": 262144,
"row_count": 58
},
"external_sourc e": {},
"synchronization": null,
"children": [],
"liked": false
},
{
"id": "99f156e2-41ea-11e5-b687-080027880ca6",
"name": "districtes_barcelona",
"display_name": "Districtes Barcelona",
"map_id": "05ba0e88-5ba9-4c54-8113-ca1ccafe83c9",
"act ive_layer_id": "00533095-d121-4485-b58e-6fef5ac64ee1",
"type": "table",
"tags": [
"Administrative regions"
],
"description": "Geometries for each one of the districts in Barcelona.",
"privacy": "PUBLIC",
"stats": {
"2015-07-20": 0,
"2015-07- 21": 0,
"2015-07-22": 0,
"2015-07-23": 0,
"2015-07-24": 0,
"2015-07-25": 0,
"2015-07-26": 0,
"2015-07-27": 0,
"2015-07-28": 0,
"2015-07-29": 0,
"2015-07-30": 0,
"2015-07-31": 0,
"2015-08-01": 0,
"2015-08-02": 0,
"2015-08-03": 0,
"2015-08- 04": 0,
"2015-08-05": 0,
"2015-08-06": 0,
"2015-08-07": 0,
"2015-08-08": 0,
"2015-08-09": 0,
"2015-08-10": 0,
"2015-08-11": 0,
"2015-08-12": 0,
"2015-08-13": 0,
"2015-08-14": 0,
"2015-08-15": 0,
"2015-08-16": 0,
"2015-08-17": 0,
"2015-08- 18": 0
},
"created_at": "2015-08-13T18:39:16+00:00",
"updated_at": "2015-08-18T08:15:13+00:00",
"permission": {
"id": "329e377b-526e-475e-b6b5-74cadd934164",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": " 99f156e2-41ea-11e5-b687-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:39:16+00:00",
"updated_at": "2015-08-13T18:39:16+00:00"
},
"locked": false,
"source": "[Barcelona Opendata](http://datos.barcelona.com/)",
"titl e": null,
"parent_id": null,
"license": "gplv2",
"attributions": null,
"kind": "geom",
"likes": 0,
"prev_id": null,
"next_id": null,
"transition_options": {
"time": 0
},
"active_child": null,
"table": {
"id": "25f3e791-2c29-4536-aeed-197465d89fa5",
"name": "public.districtes_barcelona",
"permission": {
"id": "329e377b-526e-475e-b6b5-74cadd934164",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url ": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "99f156e2-41ea-11e5-b687-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:39:16+00:00",
"updated_at": "2015-08-13T18:39:16+00:00"
},
"geometry_types": [
"ST_MultiPolygon"
],
"privacy": "PUBLIC",
"updated_at": "2015-08-13T18:39:28+00:00",
"size": 118784,
"row_count": 10
},
"e xternal_source": {},
"synchronization": null,
"children": [],
"liked": false
},
{
"id": "81ce1d70-41ea-11e5-a6d5-080027880ca6",
"name": "jamoat_tajikistan",
"display_name": "Jamoat Tajikistan",
"map_id": "e6878a94-6534-45da-9138-082b9a8e96ab ",
"active_layer_id": "3a00ad2c-f557-4529-99b7-ef4e435816df",
"type": "table",
"tags": [
"Administrative regions"
],
"description": "Geometries for jamoats (village-level self-governing units) in Tajikistan.",
"privacy": "PUBLIC",
"stats": {
"2015-07-20": 0,
"2015-07-21": 0,
"2015-07-22": 0,
"2015-07-23": 0,
"2015-07-24": 0,
"2015-07-25": 0,
"2015-07-26": 0,
"2015-07-27": 0,
"2015-07-28": 0,
"2015-07-29": 0,
"2015-07-30": 0,
"2015-07-31": 0,
"2015-08-01": 0,
"2015-08-02": 0,
"2015-08-03": 0,
"2015-08-04": 0,
"2015-08-05": 0,
"2015-08-06": 0,
"2015-08-07": 0,
"2015-08-08": 0,
"2015-08-09": 0,
"2015-08-10": 0,
"2015-08-11": 0,
"2015-08-12": 0,
"2015-08-13": 0,
"2015-08-14": 0,
"2015-08-15": 0,
"2015-08-16": 0,
"2015-08-17": 0,
"2015-08-18": 0
},
"created_at": "2015-08-13T18:38:36+00:00",
"updated_at": "2015-08-18T08:15:13+00:00",
"permission": {
"id": "d4c83c85-bd53-4f60-ad37-073750134200",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "81ce1d70-41ea-11e5-a6d5-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:38:35+00:00",
"updated_at": "2015-08-13T18:38:36+00:00"
},
"locked": false,
"source": "[Tajikistan Opendata](http://d atos.tajikistan.com/)",
"title": null,
"parent_id": null,
"license": "gplv3",
"attributions": null,
"kind": "geom",
"likes": 0,
"prev_id": null,
"next_id": null,
"transition_options": {
"time": 0
},
"active_child": null,
"table": {
"id": "b5bd2c95 -6c1d-4b22-94a0-2b9c4af19e49",
"name": "public.jamoat_tajikistan",
"permission": {
"id": "d4c83c85-bd53-4f60-ad37-073750134200",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30 @carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "81ce1d70-41ea-11e5-a6d5-0800 27880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:38:35+00:00",
"updated_at": "2015-08-13T18:38:36+00:00"
},
"geometry_types": [
"ST_MultiPolygon"
],
"privacy": "PUBLIC",
"updated_at": "2015-08-13T18:38:47+00:00",
"size": 380928,
"row_count": 351
},
"external_source": {},
"synchronization": null,
"children": [],
"liked": false
},
{
"id": "6cc2d01a-41ea-11e5-9eef-080027880ca6",
"name": "barris_barcelona",
"display_name": "Barrios Barcelona",
"map_id": "af479e52-7 cd6-4baf-aa38-1cb119454f73",
"active_layer_id": "df8460d3-d3d4-44ae-85d5-9e5656e45a32",
"type": "table",
"tags": [
"Administrative regions"
],
"description": "Geometries for Barcelona neighborhoods.",
"privacy": "PUBLIC",
"stats": {
"2015-07 -20": 0,
"2015-07-21": 0,
"2015-07-22": 0,
"2015-07-23": 0,
"2015-07-24": 0,
"2015-07-25": 0,
"2015-07-26": 0,
"2015-07-27": 0,
"2015-07-28": 0,
"2015-07-29": 0,
"2015-07-30": 0,
"2015-07-31": 0,
"2015-08-01": 0,
"2015-08-02": 0,
"2015-08 -03": 0,
"2015-08-04": 0,
"2015-08-05": 0,
"2015-08-06": 0,
"2015-08-07": 0,
"2015-08-08": 0,
"2015-08-09": 0,
"2015-08-10": 0,
"2015-08-11": 0,
"2015-08-12": 0,
"2015-08-13": 0,
"2015-08-14": 0,
"2015-08-15": 0,
"2015-08-16": 0,
"2015-08 -17": 0,
"2015-08-18": 0
},
"created_at": "2015-08-13T18:38:00+00:00",
"updated_at": "2015-08-18T08:15:13+00:00",
"permission": {
"id": "9bc70a85-029f-4e57-8f11-8bae254ef1cc",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"usernam e": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "6cc2d01a-41ea-11e5-9eef-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:38:00+00:00",
"updated_at": "2015-08-13T18:38:00+00:00"
},
"locked": false,
"source": "[Barcelona Opendata](http://datos.barce lona.com/)",
"title": null,
"parent_id": null,
"license": "",
"attributions": null,
"kind": "geom",
"likes": 0,
"prev_id": null,
"next_id": null,
"transition_options": {
"time": 0
},
"active_child": null,
"table": {
"id": "78612fa1-1eb1-4935-a181- aedbbafe3ce7",
"name": "public.barris_barcelona",
"permission": {
"id": "9bc70a85-029f-4e57-8f11-8bae254ef1cc",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"a vatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "6cc2d01a-41ea-11e5-9eef-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:38:00+00:00",
"updated_at": "2015-08-13T18:38:00+00:00"
},
"geometry_types": [
"ST_MultiPolygon"
],
"privacy": "PUBLIC",
"updated_at": "2015-08-13T18:38:14+00:00",
"size": 397312,
"row_count": 73
},
"external_source": {},
"synchronization": null,
"children": [],
"liked": false
},
{
"id": "d6387acc-41ea-11e5-a825-080027880ca6",
"name": "meta_dataset",
"display_name": null,
"map_id": "121040fa-f4c3-4e74-8c17-3f8905d65b08",
"active_l ayer_id": "f0fcfd45-b8e9-4753-920e-2394b4556af2",
"type": "table",
"tags": [],
"description": null,
"privacy": "PUBLIC",
"stats": {
"2015-07-20": 0,
"2015-07-21": 0,
"2015-07-22": 0,
"2015-07-23": 0,
"2015-07-24": 0,
"2015-07-25": 0,
"2015- 07-26": 0,
"2015-07-27": 0,
"2015-07-28": 0,
"2015-07-29": 0,
"2015-07-30": 0,
"2015-07-31": 0,
"2015-08-01": 0,
"2015-08-02": 0,
"2015-08-03": 0,
"2015-08-04": 0,
"2015-08-05": 0,
"2015-08-06": 0,
"2015-08-07": 0,
"2015-08-08": 0,
"2015- 08-09": 0,
"2015-08-10": 0,
"2015-08-11": 0,
"2015-08-12": 0,
"2015-08-13": 0,
"2015-08-14": 0,
"2015-08-15": 0,
"2015-08-16": 0,
"2015-08-17": 0,
"2015-08-18": 0
},
"created_at": "2015-08-13T18:40:57+00:00",
"updated_at": "2015-08-14T09:3 0:49+00:00",
"permission": {
"id": "2ac3b2f2-2b0c-45a1-8323-fe0159831a45",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/av atar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "d6387acc-41ea-11e5-a825-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2 015-08-13T18:40:57+00:00",
"updated_at": "2015-08-13T18:40:57+00:00"
},
"locked": false,
"source": null,
"title": null,
"parent_id": null,
"license": null,
"attributions": null,
"kind": "geom",
"likes": 0,
"prev_id": null,
"next_id": null,
"trans ition_options": {
"time": 0
},
"active_child": null,
"table": {
"id": "d713b741-32b0-4430-a8e3-2fdb2ddda85c",
"name": "public.meta_dataset",
"permission": {
"id": "2ac3b2f2-2b0c-45a1-8323-fe0159831a45",
"owner": {
"id": "beacfd17-418e-4e71-b307 -95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_si ze_in_bytes": 2387968
},
"entity": {
"id": "d6387acc-41ea-11e5-a825-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:40:57+00:00",
"updated_at": "2015-08-13T18:40:57+00:00"
},
"geometry_types": [],
"privacy": "PUBLIC",
"updated_at": "2015-08-14T08:26:25+00:00",
"size": 24576,
"row_count": 5
},
"external_source": {},
"synchronization": null,
"children": [],
"liked": false
},
{
"id":"31636cc4-4fb8-11e5-98ec-0e8dde98a187",
"name":"nycpluto_all",
"display_name":"NYC MapPLUTO - All NY",
"map_id":null,
"active_layer_id":null,
"type":"remote",
"tags":[
"Building footprints"
],
"description":"MapPLUTO tax lot data for New York.",
"privacy":"PUBLIC",
"stats":{
"2015-08-24":0,
"2015-08-25":0,
"2015-08-26":0,
"2015-08-27":0,
"2015-08-28":0,
"2015-08-29":0,
"2015-08-30":0,
"2015-08-31":0,
"2015-09-01":0,
"2015-09-02":0,
"2015-09-03":0,
"2015-09-04":0,
"2015-09-05":0,
"2015-09-06":0,
"2015-09-07":0,
"2015-09-08":0,
"2015-09-09":0,
"2015-09-10":0,
"2015-09-11":0,
"2015-09-12":0,
"2015-09-13":0,
"2015-09-14":0,
"2015-09-15":0,
"2015-09-16":0,
"2015-09-17":0,
"2015-09-18":0,
"2015-09-19":0,
"2015-09-20":0,
"2015-09-21":0,
"2015-09-22":0
},
"created_at":"2015-08-31T08:13:42+00:00",
"updated_at":"2015-08-31T08:13:42+00:00",
"permission":{
"id":"2f2d73c9-d60e-4eb5-8031-be7059878d4b",
"owner":{
"id": "beacfd17-418e-4e71-b307 -95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_si ze_in_bytes": 2387968
},
"entity":{
"id":"31636cc4-4fb8-11e5-98ec-0e8dde98a187",
"type":"vis"
},
"acl":[],
"created_at":"2015-08-31T08:13:42+00:00",
"updated_at":"2015-08-31T08:13:42+00:00"
},
"locked":false,
"source":"[NYC Planning](http://www.nyc.gov/html/dcp/home.html)",
"title":null,
"parent_id":null,
"license": "public_domain",
"attributions":null,
"kind":"geom",
"likes":0,
"prev_id":null,
"next_id":null,
"transition_options":{
"time": 0
},
"active_child":null,
"table":{
"id": "36612fa1-1eb1-4935-a181-5edbbafe3ce7",
"name": "public.nycpluto_all",
"permission": {
"id": "9bc70a85-029f-4e57-8f11-8bae254ef1cc",
"owner": {
"id": "beacfd17-418e-4e71-b307-95b5c96105dc",
"username": "common-data",
"email": "mario.defrutos+30@carto.com",
"avatar_url": "//example.com/avatars/avatar_mountains_blue.png",
"base_url": "http://common-data.localhost.lan:3000",
"quota_in_bytes": 262144000,
"db_size_in_bytes": 2387968
},
"entity": {
"id": "6cc2d01a-41ea-11e5-9eef-080027880ca6",
"type": "vis"
},
"acl": [],
"created_at": "2015-08-13T18:38:00+00:00",
"updated_at": "2015-08-13T18:38:00+00:00"
},
"geometry_types": [
"ST_MultiPolygon"
],
"privacy": "PUBLIC",
"updated_at": "2015-08-21T18:38:14+00:00",
"size": 397012,
"row_count": 70
},
"external_source":{},
"synchronization":null,
"children":[],
"liked":false
}
],
"total_entries": 6
}
response
INVALID_JSON_RESPONSE = '{'
end
+28
View File
@@ -0,0 +1,28 @@
require_relative '../spec_helper'
require 'ruby-debug'
describe DataImport do
before(:all) do
::User.all.each(&:destroy)
@user = create_user(:username => 'test', :email => "client@example.com", :password => "clientex")
@table = create_table :user_id => @user.id
end
after(:all) do
bypass_named_maps
@user.destroy
end
folder = ENV['TEST_FILES'] || File.join(File.dirname(__FILE__), '../support/data/')
Dir[folder + '/*'].each do |file|
it "imports #{file}" do
data_import = DataImport.create(
:user_id => @user.id,
:data_source => '/../'+Pathname.new(file).relative_path_from(Pathname.new(Rails.root.to_s)).to_s,
:updated_at => Time.now
).run_import!
table = Table[data_import.table_id]
table.should_not be_nil
end
end
end
@@ -0,0 +1,22 @@
# Including specs must define `:data_import_class`
shared_examples_for 'DataImport model' do
describe 'validation' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
end
after(:all) do
@user.destroy if @user
end
it 'only allows valid collision strategies' do
data_import = data_import_class.new(collision_strategy: 'wadus', user_id: @user.id)
data_import.valid?.should be_false
data_import.errors[:collision_strategy].should_not be_nil
data_import = data_import_class.new(collision_strategy: DataImport::COLLISION_STRATEGY_SKIP, user_id: @user.id)
data_import.valid?.should be_true
data_import.errors[:collision_strategy].should be_empty
end
end
end
+666
View File
@@ -0,0 +1,666 @@
require_relative '../spec_helper'
require_relative '../helpers/file_server_helper'
require_relative 'data_import_shared_examples'
require_relative '../../lib/carto/ghost_tables_manager'
describe DataImport do
let(:data_import_class) { ::DataImport }
it_behaves_like 'DataImport model'
before(:each) do
@user = create_user
bypass_named_maps
@table = create_table(user_id: @user.id)
end
after(:each) do
@user.destroy
end
after(:all) do
bypass_named_maps
@user.try(:destroy)
end
it "raises an 1014 error when strategy is set to skip and there's already a table with that name" do
table1 = create_table(user_id: @user.id)
table1.insert_row!(name: 1.0)
table1.insert_row!(name: 2.0)
query = "select * from #{table1.name}"
data_import = DataImport.create(
user_id: @user.id,
table_name: 'target_table',
from_query: query
)
data_import.run_import!
data_import.state.should eq 'complete'
data_import = DataImport.create(
user_id: @user.id,
table_name: 'target_table',
from_query: query,
collision_strategy: 'skip'
)
data_import.run_import!
data_import.state.should eq 'complete'
data_import.error_code.should eq 1022
end
it 'raises an 8004 error when merging tables
through columns with different types' do
table1 = create_table(user_id: @user.id)
table2 = create_table(user_id: @user.id)
table1.modify_column!(name: 'name', type: 'double precision')
table1.insert_row!(name: 1.0)
table2.insert_row!(name: '1')
merge_query = %(
SELECT #{table2.name}.the_geom,
#{table2.name}.description,
#{table2.name}.name,
#{table1.name}.the_geom AS #{table1.name}_the_geom,
#{table1.name}.description AS #{table1.name}_description,
#{table1.name}.name AS #{table1.name}_name
FROM #{table2.name} FULL OUTER JOIN #{table1.name}
ON #{table2.name}.name = #{table1.name}.name
)
data_import = DataImport.create(
user_id: @user.id,
table_name: "merged_table",
from_query: merge_query
)
data_import.run_import!
data_import.error_code.should == 8004
end
it 'raises a meaningful error if cartodb_id is not valid' do
Table.any_instance.stubs(:cartodbfy).raises(CartoDB::CartoDBfyInvalidID)
data_import = DataImport.create(
user_id: @user.id,
data_source: fake_data_path('clubbing.csv'),
updated_at: Time.now
)
data_import.run_import!
data_import.error_code.should == 2011
end
it 'should overwrite dataset if collision_strategy is set to overwrite' do
carto_user = Carto::User.find(@user.id)
carto_user.visualizations.count.should eq 1
data_import = create_import(overwrite: false, truncated: false)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 2
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 3176
user_tables_should_be_registered
data_import = create_import(overwrite: false, truncated: false)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 3
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon_1'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 3176
user_tables_should_be_registered
data_import = create_import(overwrite: true, truncated: true)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 3
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 2
user_tables_should_be_registered
end
it 'should overwrite dataset with query if collision_strategy is set to overwrite' do
# overwriting from a sql is a different case needs to be tackled -> https://github.com/CartoDB/cartodb/issues/13139
carto_user = Carto::User.find(@user.id)
carto_user.visualizations.count.should eq 1
data_import = create_import(overwrite: false, truncated: false)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 2
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 3176
user_tables_should_be_registered
query = 'select * from walmart_latlon limit 1'
data_import = create_import_from_query(overwrite: true, from_query: query)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 2
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 1
user_tables_should_be_registered
end
it 'should raise an error if overwriting with non existent table in query' do
data_import = create_import_from_query(
overwrite: true,
from_query: 'select * from walmart_latlon_wtf limit 1'
)
data_import.run_import!
data_import.state.should eq 'failure'
data_import.error_code.should eq 8003
data_import.log.entries.should match(/relation.*does not exist/)
end
it 'should raise an exception if overwriting with missing data' do
carto_user = Carto::User.find(@user.id)
carto_user.visualizations.count.should eq 1
data_import = create_import(overwrite: false, truncated: true)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 2
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 2
user_tables_should_be_registered
data_import = create_import(overwrite: true, truncated: true, incomplete_schema: true)
expect { data_import.run_import! }.to raise_error(::CartoDB::Importer2::IncompatibleSchemas)
data_import.log.entries.should match(/Exception: Incompatible Schemas/)
end
describe 'organization behaviour' do
include_context 'organization with users helper'
it 'should overwrite even in users with hyphens in the schema name' do
carto_user = Carto::User.find(@org_user_owner.id)
expect(carto_user.username).to include '-' # Fixture check
data_import = create_import(user: @org_user_owner, overwrite: false, truncated: false)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 1
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 3176
user_tables_should_be_registered
data_import = create_import(user: @org_user_owner, overwrite: true, truncated: true)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 1
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 2
user_tables_should_be_registered
end
end
it 'should not raise exceptions if overwriting with more data' do
carto_user = Carto::User.find(@user.id)
carto_user.visualizations.count.should eq 1
data_import = create_import(overwrite: false, truncated: true, incomplete_schema: true)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 2
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 2
user_tables_should_be_registered
data_import = create_import(overwrite: true, truncated: true, incomplete_schema: false)
data_import.run_import!
carto_user.reload
carto_user.visualizations.count.should eq 2
data_import.state.should eq 'complete'
data_import.table_name.should eq 'walmart_latlon'
data_import.user.in_database["select count(*) from #{data_import.table_name}"].all[0][:count].should eq 2
user_tables_should_be_registered
end
def user_tables_should_be_registered
Carto::GhostTablesManager.new(@user.id).user_tables_synced_with_db?.should eq(true), "Tables not properly registered"
end
def create_import(user: @user, overwrite:, truncated:, incomplete_schema: false)
DataImport.create(
user_id: user.id,
data_source: Rails.root.join("spec/support/data/#{truncated ? 'truncated/' : ''}#{incomplete_schema ? 'incomplete_schema/' : ''}walmart_latlon.csv").to_s,
data_type: "file",
table_name: 'walmart_latlon',
state: "pending",
success: false,
updated_at: Time.now,
created_at: Time.now,
original_url: Rails.root.join("spec/support/data/walmart_latlon.csv").to_s,
cartodbfy_time: 0.0,
collision_strategy: overwrite ? 'overwrite' : nil
)
end
def create_import_from_query(user: @user, overwrite:, from_query: '')
DataImport.create(
user_id: user.id,
data_source: from_query,
from_query: from_query,
data_type: "query",
table_name: 'walmart_latlon',
state: "pending",
success: false,
updated_at: Time.now,
created_at: Time.now,
original_url: Rails.root.join("spec/support/data/walmart_latlon.csv").to_s,
cartodbfy_time: 0.0,
collision_strategy: overwrite ? 'overwrite' : nil
)
end
it 'raises a meaningful error if over storage quota' do
previous_quota_in_bytes = @user.quota_in_bytes
@user.quota_in_bytes = 0
@user.save
data_import = DataImport.create(
user_id: @user.id,
data_source: fake_data_path('clubbing.csv'),
updated_at: Time.now
).run_import!
@user.quota_in_bytes = previous_quota_in_bytes
@user.save
data_import.error_code.should == 8001
end
it 'raises a meaningful error if over table quota' do
previous_table_quota = @user.table_quota
@user.table_quota = 0
@user.save
data_import = DataImport.create(
user_id: @user.id,
data_source: fake_data_path('clubbing.csv'),
updated_at: Time.now
).run_import!
@user.table_quota = previous_table_quota
@user.save
data_import.error_code.should == 8002
end
it 'should allow to duplicate an existing table' do
data_import = DataImport.create(
user_id: @user.id,
table_name: 'duplicated_table',
updated_at: Time.now,
table_copy: @table.name).run_import!
data_import.data_type.should eq 'query'
duplicated_table = ::UserTable.where(id: data_import.table_id).first
duplicated_table.should_not be_nil
duplicated_table.name.should be == 'duplicated_table'
end
it 'should allow to create a table from a query' do
data_import_1 = DataImport.create(
user_id: @user.id,
data_source: fake_data_path('clubbing.csv'),
updated_at: Time.now).run_import!
data_import_1.state.should be == 'complete'
data_import_2 = DataImport.create(
user_id: @user.id,
table_name: 'from_query',
updated_at: Time.now,
from_query: "SELECT * FROM #{data_import_1.table_name} LIMIT 5").run_import!
data_import_2.state.should be == 'complete'
data_import_2.data_type.should eq 'query'
duplicated_table = ::UserTable.where(id: data_import_2.table_id).first
duplicated_table.should_not be_nil
duplicated_table.name.should be == 'from_query'
duplicated_table.service.records[:rows].should have(5).items
end
it 'imports a simple file' do
data_import = DataImport.create(
user_id: @user.id,
data_source: fake_data_path('clubbing.csv'),
updated_at: Time.now
).run_import!
table = ::UserTable.where(id: data_import.table_id).first
table.should_not be_nil
table.name.should be == 'clubbing'
table.service.records[:rows].should have(10).items
end
it 'imports a simple file with latlon' do
data_import = DataImport.create(
user_id: @user.id,
data_source: Rails.root.join('services/importer/spec/fixtures/csv_with_geojson.csv').to_s,
updated_at: Time.now
).run_import!
table = ::UserTable.where(id: data_import.table_id).first
table.should_not be_nil
end
it 'imports a gpkg with no coordinate system' do
data_import = DataImport.create(
user_id: @user.id,
data_source: fake_data_path('no_coordinate_system_dataset.gpkg'),
updated_at: Time.now
).run_import!
table = ::UserTable.where(id: data_import.table_id).first
table.should_not be_nil
table.name.should be == 'no_coordinate_system_dataset'
table.service.records[:rows].should have(1).items
end
it 'should allow to create a table from a url' do
data_import = nil
CartoDB::Importer2::Downloader.any_instance.stubs(:validate_url!).returns(true)
serve_file Rails.root.join('db/fake_data/clubbing.csv') do |url|
data_import = DataImport.create(
user_id: @user.id,
data_source: url,
updated_at: Time.now).run_import!
end
table = ::UserTable.where(id: data_import.table_id).first
table.should_not be_nil
table.name.should be == 'clubbing'
table.service.records[:rows].should have(10).items
end
it 'updates synch_job state after success data import' do
data_import = nil
sync_job = FactoryGirl.create(:enqueued_sync)
Carto::Synchronization.find(sync_job.id).state.should eq Carto::Synchronization::STATE_QUEUED
CartoDB::Importer2::Downloader.any_instance.stubs(:validate_url!).returns(true)
serve_file Rails.root.join('db/fake_data/clubbing.csv') do |url|
data_import = DataImport.create(
user_id: @user.id,
data_source: url,
synchronization_id: sync_job.id,
updated_at: Time.now
).run_import!
end
data_import.state.should eq DataImport::STATE_COMPLETE
Carto::Synchronization.find(sync_job.id).state.should eq Carto::Synchronization::STATE_SUCCESS
end
it 'updates synch_job state after failed data import' do
sync_job = FactoryGirl.create(:enqueued_sync)
Carto::Synchronization.find(sync_job.id).state.should eq Carto::Synchronization::STATE_QUEUED
data_import = DataImport.create(
user_id: @user.id,
data_source: "http://mydatasource.cartodb.wadus.com/foo.csv",
synchronization_id: sync_job.id,
updated_at: Time.now
).run_import!
data_import.state.should eq DataImport::STATE_FAILURE
Carto::Synchronization.find(sync_job.id).state.should eq Carto::Synchronization::STATE_FAILURE
end
it 'should allow to create a table from a url with params' do
data_import = nil
CartoDB::Importer2::Downloader.any_instance.stubs(:validate_url!).returns(true)
serve_file Rails.root.join('db/fake_data/clubbing.csv?param=wadus'),
headers: { "content-type" => "text/plain" } do |url|
data_import = DataImport.create(
user_id: @user.id,
data_source: url,
updated_at: Time.now).run_import!
end
table = ::UserTable.where(id: data_import.table_id).first
table.should_not be_nil
table.name.should be == 'clubbing'
table.service.records[:rows].should have(10).items
end
it "can create a table from a query selecting only the cartodb_id" do
data_import_1 = DataImport.create(
user_id: @user.id,
data_source: fake_data_path('clubbing.csv'),
updated_at: Time.now).run_import!
data_import_1.state.should be == 'complete'
data_import_2 = DataImport.create(
user_id: @user.id,
table_name: 'from_query',
updated_at: Time.now,
from_query: "SELECT cartodb_id FROM #{data_import_1.table_name} LIMIT 5").run_import!
data_import_2.state.should be == 'complete'
duplicated_table = ::UserTable.where(id: data_import_2.table_id).first
duplicated_table.should_not be_nil
duplicated_table.name.should be == 'from_query'
duplicated_table.service.records[:rows].should have(5).items
end
it "should remove any uploaded files after deletion" do
upload_path = FileUtils.mkdir_p Rails.root.join('public', 'uploads', 'test0000000000000000')
file_path = File.join(upload_path, 'wadus.csv')
FileUtils.cp Rails.root.join('db/fake_data/clubbing.csv'), file_path
data_import = DataImport.create(
user_id: @user.id,
data_source: file_path,
updated_at: Time.now)
data_import.destroy
Dir.exists?(file_path).should be_false
end
it 'should add a common_data extra_option' do
DataImport.any_instance.stubs(:from_common_data?).returns(true)
data_import = DataImport.create(
user_id: @user.id,
data_source: "http://127.0.0.1/foo.csv"
)
data_import.reload
data_import.extra_options[:common_data].should eq true
end
it 'should know that the import is from common data' do
Cartodb.with_config(common_data: { 'username' => 'mycommondata', 'host' => 'cartodb.wadus.com' }) do
data_import = DataImport.create(
user_id: @user.id,
data_source: "http://mycommondata.cartodb.wadus.com/foo.csv"
)
data_import.from_common_data?.should eq true
end
end
it 'should not consider a import as common data if common_data config does not exist' do
Cartodb.with_config(common_data: nil) do
data_import = DataImport.create(
user_id: @user.id,
data_source: "http://mycommondata.cartodb.wadus.com/foo.csv"
)
data_import.from_common_data?.should eq false
end
end
it 'should not consider a import as common data if common_data config does not match with url' do
Cartodb.with_config(common_data: { 'username' => 'mycommondata', 'host' => 'cartodb.wadus.com' }) do
data_import = DataImport.create(
user_id: @user.id,
data_source: "http://mydatasource.cartodb.wadus.com/foo.csv"
)
data_import.from_common_data?.should eq false
end
end
it 'mark as failure a stuck job' do
data_import = DataImport.create(
user_id: @user.id,
data_source: "http://mydatasource.cartodb.wadus.com/foo.csv",
state: DataImport::STATE_STUCK
)
data_import.mark_as_failed_if_stuck!.should eq true
data_import.success.should eq false
data_import.error_code.should eq 6671
data_import.mark_as_failed_if_stuck!.should eq false
end
describe 'arcgis connector' do
after :each do
CartoDB::Importer2::QueryBatcher.any_instance.unstub(:execute_update)
end
it 'should complete with a warning when the query batcher raises a timeout exception' do
stub_arcgis_response_with_file(File.expand_path('spec/fixtures/arcgis_response_valid.json'))
CartoDB::Importer2::QueryBatcher.any_instance
.stubs(:execute_update)
.raises(Sequel::DatabaseError, 'canceling statement due to statement timeout')
data_import = DataImport.create(
user_id: @user.id,
service_name: 'arcgis',
service_item_id: 'https://wtf.com/arcgis/rest/services/Planning/EPI_Primary_Planning_Layers/MapServer/2'
)
data_import.run_import!
data_import.state.should eq 'complete'
data_import.log.entries.should include 'Error fixing geometries during import, skipped'
end
it 'should raise invalid data error when the query batcher raise any other exception' do
stub_arcgis_response_with_file(File.expand_path('spec/fixtures/arcgis_response_valid.json'))
CartoDB::Importer2::QueryBatcher.any_instance
.stubs(:execute_update)
.raises(Sequel::DatabaseError, 'GEOSisValid(): InterruptedException: Interrupted!')
data_import = DataImport.create(
user_id: @user.id,
service_name: 'arcgis',
service_item_id: 'https://wtf.com/arcgis/rest/services/Planning/EPI_Primary_Planning_Layers/MapServer/2'
)
data_import.run_import!
data_import.state.should eq 'failure'
data_import.error_code.should eq 1012
end
it 'should import this supposed invalid dataset for ogr2ogr 2.1.1' do
stub_arcgis_response_with_file(File.expand_path('spec/fixtures/arcgis_response_invalid.json'))
data_import = DataImport.create(
user_id: @user.id,
service_name: 'arcgis',
service_item_id: 'https://wtf.com/arcgis/rest/services/Planning/EPI_Primary_Planning_Layers/MapServer/2'
)
data_import.run_import!
data_import.state.should eq 'complete'
end
it 'fail with error 1020 if timeout' do
Typhoeus::Response.any_instance.stubs(:timed_out?).returns(true)
stub_arcgis_response_with_file(File.expand_path('spec/fixtures/arcgis_response_missing_ogc_fid.json'))
data_import = DataImport.create(
user_id: @user.id,
service_name: 'arcgis',
service_item_id: 'https://wtf.com/arcgis/rest/services/Planning/EPI_Primary_Planning_Layers/MapServer/2'
)
data_import.run_import!
data_import.state.should eq 'failure'
data_import.error_code.should eq 1020
Typhoeus::Response.unstub(:timed_out?)
end
it 'should import files with missing ogc_fid' do
stub_arcgis_response_with_file(File.expand_path('spec/fixtures/arcgis_response_missing_ogc_fid.json'))
data_import = DataImport.create(
user_id: @user.id,
service_name: 'arcgis',
service_item_id: 'https://wtf.com/arcgis/rest/services/Planning/EPI_Primary_Planning_Layers/MapServer/2'
)
data_import.run_import!
data_import.state.should eq 'complete'
end
end
describe 'log' do
it 'is initialized to a CartoDB::Log instance' do
data_import = DataImport.create(
user_id: @user.id,
data_source: "http://mydatasource.cartodb.wadus.com/foo.csv"
)
data_import.log.should be_instance_of CartoDB::Log
end
it 'allows messages to be appended' do
data_import = DataImport.new(
user_id: @user.id,
table_name: 'foo',
from_query: 'bogus'
)
data_import.log.append('sample message')
data_import.save
data_import.log.to_s.should =~ /sample message/
end
it 'is fetched after retrieving the data_import object from DB' do
data_import = DataImport.new(
user_id: @user.id,
table_name: 'foo',
from_query: 'bogus'
)
data_import.log.append('sample message')
# Logs get saved at checkpoints or certain operations, so force store
data_import.log.store
data_import.save
rehydrated_data_import = DataImport[id: data_import.id]
data_import.log.to_s.should == rehydrated_data_import.log.to_s
end
it 'will not overwrite an existing logger field' do
data_import = DataImport.new(
user_id: @user.id,
table_name: 'foo',
from_query: 'bogus',
)
data_import.save
data_import.logger = 'existing log'
data_import.this.update(logger: 'existing log')
data_import.logger.should == 'existing log'
data_import.log.append('sample message')
data_import.log.to_s.should =~ /sample message/
data_import.save
data_import.logger.should == 'existing log'
end
end
context 'viewer users' do
after(:each) do
@user.viewer = false
@user.save
end
it "can't create new data imports" do
@user.viewer = true
@user.save
data_import = DataImport.new(
user_id: @user.id,
table_name: 'fromviewer',
from_query: 'fromviewer_q'
)
expect { data_import.save }.to raise_error(Sequel::ValidationFailed, "user Viewer users can't create data imports")
end
end
end
+364
View File
@@ -0,0 +1,364 @@
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
+155
View File
@@ -0,0 +1,155 @@
require_relative '../../spec_helper'
require 'rspec/core'
require 'ostruct'
require_relative '../../../app/models/layer/presenter'
include CartoDB
describe CartoDB::LayerModule::Presenter do
describe '#to_vizjson_v2' do
it 'wraps the sql if query_wrapper available' do
layer = OpenStruct.new(
public_values: { 'options' => { 'visible' => nil } },
tooltip: { 'template' => 'blahblah' },
kind: 'carto',
options: {
'query' => '',
'tile_style' => '',
'interactivity' => '',
'style_version' => '',
'table_name' => '000cd294-b124-4f82-b569-0f7fe41d2db8',
'query_wrapper' => 'bogus tpl <%= sql %>'
}
)
vizjson = CartoDB::LayerModule::Presenter.new(layer).to_vizjson_v2
vizjson.fetch(:options).fetch(:sql).should eq 'bogus tpl select * from "000cd294-b124-4f82-b569-0f7fe41d2db8"'
layer = OpenStruct.new(
public_values: { 'options' => { 'visible' => nil } },
tooltip: { 'template' => 'blahblah' },
kind: 'carto',
options: {
'query' => '',
'tile_style' => '',
'interactivity' => '',
'style_version' => '',
'table_name' => 'bogus_table',
'query_wrapper' => 'bogus template <%= sql %>'
}
)
vizjson = CartoDB::LayerModule::Presenter.new(layer).to_vizjson_v2
vizjson.fetch(:options).fetch(:sql).should eq 'bogus template select * from bogus_table'
layer = OpenStruct.new(
public_values: { 'options' => {'visible' => nil} },
tooltip: { 'template' => 'blahblah' },
kind: 'carto',
options: {
'query' => 'select the_geom from bogus_table',
'tile_style' => '',
'interactivity' => '',
'style_version' => '',
'table_name' => 'bogus_table',
'query_wrapper' => 'bogus template <%= sql %>'
}
)
vizjson = CartoDB::LayerModule::Presenter.new(layer).to_vizjson_v2
vizjson.fetch(:options).fetch(:sql)
.should == 'bogus template select the_geom from bogus_table'
end
it 'returns `sql_wrap` with `query_wrapper` if present and with style properties nil or not autogenerated' do
query_wrapper = 'bogus template <%= sql %>'
layer = OpenStruct.new(
public_values: { 'options' => { 'visible' => nil } },
kind: 'carto',
options: {
'interactivity' => '',
'style_version' => '',
'table_name' => 'bogus_table',
'query_wrapper' => query_wrapper,
'style_properties' => {}
}
)
vizjson = CartoDB::LayerModule::Presenter.new(layer).to_vizjson_v2
vizjson[:options]['sql_wrap'].should eq query_wrapper
layer = OpenStruct.new(
public_values: { 'options' => { 'visible' => nil } },
kind: 'carto',
options: {
'interactivity' => '',
'style_version' => '',
'table_name' => 'bogus_table',
'query_wrapper' => query_wrapper,
'style_properties' => { 'autogenerated' => false }
}
)
vizjson = CartoDB::LayerModule::Presenter.new(layer).to_vizjson_v2
vizjson[:options]['sql_wrap'].should eq query_wrapper
end
it 'does not return `sql_wrap` with `query_wrapper` if present without style properties or autogenerated' do
query_wrapper = 'bogus template <%= sql %>'
layer = OpenStruct.new(
public_values: { 'options' => { 'visible' => nil } },
kind: 'carto',
options: {
'interactivity' => '',
'style_version' => '',
'table_name' => 'bogus_table',
'query_wrapper' => query_wrapper
}
)
vizjson = CartoDB::LayerModule::Presenter.new(layer).to_vizjson_v2
vizjson[:options]['sql_wrap'].should be_nil
layer = OpenStruct.new(
public_values: { 'options' => { 'visible' => nil } },
kind: 'carto',
options: {
'interactivity' => '',
'style_version' => '',
'table_name' => 'bogus_table',
'query_wrapper' => query_wrapper,
'style_properties' => { 'autogenerated' => true }
}
)
vizjson = CartoDB::LayerModule::Presenter.new(layer).to_vizjson_v2
vizjson[:options]['sql_wrap'].should be_nil
end
it 'keeps the visible attribute when rendering' do
layer = OpenStruct.new(
kind: 'torque',
options: {
'query' => 'select the_geom from bogus_table',
'tile_style' => '',
'interactivity' => '',
'style_version' => '',
'table_name' => 'bogus_table',
'query_wrapper' => 'bogus template <%= sql %>',
'visible' => true
},
)
options = {
visualization_id: '',
sql: '',
}
vizjson = CartoDB::LayerModule::Presenter.new(layer, options).to_vizjson_v2
vizjson[:options]['visible'].should == true
end
end
end
+319
View File
@@ -0,0 +1,319 @@
shared_examples_for 'Layer model' do
context "setups" do
it "should be preloaded with the correct default values" do
l = layer_class.create(Cartodb.config[:layer_opts]["data"]).reload
l.kind.should == 'carto'
l.options.should == Cartodb.config[:layer_opts]["data"]["options"]
l = layer_class.create(Cartodb.config[:layer_opts]["background"]).reload
l.kind.should == 'background'
l.options.should == Cartodb.config[:layer_opts]["background"]["options"]
end
it "should not allow to create layers of unkown types" do
expect { layer_class.create(kind: "wadus").save! }.to raise_error
end
it "should allow to be linked to many maps" do
table2 = Table.new
table2.user_id = @user.id
table2.save
layer = layer_class.create(kind: 'carto')
map = create_map(user_id: @user.id, table_id: @table.id)
map2 = create_map(user_id: @user.id, table_id: table2.id)
add_layer_to_entity(map, layer)
add_layer_to_entity(map2, layer)
layer.reload
map.layers.first.id.should == layer.id
map2.layers.first.id.should == layer.id
layer.maps.should include(map, map2)
end
it "should allow to be linked to many users" do
layer = layer_class.create(kind: 'carto')
add_layer_to_entity(@user, layer)
@user.reload.layers.map(&:id).should include(layer.id)
layer.users.map(&:id).should include(@user.id)
end
it "should set default order when adding layers to a map" do
map = create_map(user_id: @user.id, table_id: @table.id)
5.times do |i|
layer = layer_class.create(kind: 'carto')
add_layer_to_entity(map, layer)
layer.reload.order.should == i
end
end
it "should set default order when adding layers to a user" do
@user.layers.each(&:destroy)
@user.reload
5.times do |i|
layer = layer_class.create(kind: 'carto')
add_layer_to_entity(@user, layer)
layer.reload.order.should == i
end
end
context "when the type is cartodb and the layer is updated" do
before do
@map = create_map(user_id: @user.id, table_id: @table.id)
@layer = layer_class.create(kind: 'carto', options: { query: "select * from #{@table.name}" })
add_layer_to_entity(@map, @layer)
end
it "should invalidate its maps" do
CartoDB::Varnish.any_instance.stubs(:purge).returns(true)
@layer.maps.count.should eq 1
@layer.maps.each do |map|
map.expects(:notify_map_change).times(1)
end
@layer.save
end
end
context "when the type is not cartodb" do
before do
@map = create_map(user_id: @user.id, table_id: @table.id)
@layer = layer_class.create(kind: 'tiled')
add_layer_to_entity(@map, @layer)
end
it "should not invalidate its related tables varnish cache" do
@layer.maps.each do |map|
map.expects(:notify_map_change).times(1)
end
@layer.send(:affected_tables).each do |table|
table.expects(:update_cdb_tablemetadata).times(0)
end
@layer.save
end
end
it "should update updated_at after saving" do
layer = layer_class.create(kind: 'carto')
after = layer.updated_at
Delorean.jump(1.minute)
layer.options[:query] = 'SELECT * FROM arbitrary_change'
layer.save
after.should < layer.updated_at
Delorean.back_to_the_present
end
it "should correctly identify affected tables" do
table2 = Table.new
table2.user_id = @user.id
table2.save
map = create_map(user_id: @user.id, table_id: @table.id)
layer = layer_class.create(
kind: 'carto',
options: { query: "select * from #{@table.name}, #{table2.name};select 1;select * from #{table2.name}" }
)
add_layer_to_entity(map, layer)
layer.reload
layer.send(:affected_tables).map(&:name).should =~ [table2.name, @table.name]
end
it "should return empty affected tables when no tables are involved" do
map = create_map(user_id: @user.id, table_id: @table.id)
layer = layer_class.create(
kind: 'carto',
options: { query: "select 1" }
)
add_layer_to_entity(map, layer)
layer.send(:affected_tables).map(&:name).should =~ []
end
it 'includes table_name option in the results' do
map = create_map(user_id: @user.id, table_id: @table.id)
layer = layer_class.create(
kind: 'carto',
options: { query: "select 1", table_name: @table.name }
)
add_layer_to_entity(map, layer)
layer.reload
layer.send(:affected_tables).map(&:name).should =~ [@table.name]
end
end
describe '#base_layer?' do
it 'returns true if its kind is a base layer' do
layer = layer_class.new(kind: 'tiled')
layer.base_layer?.should == true
end
end
describe '#data_layer?' do
it 'returns true if its of a carto kind' do
layer = layer_class.new(kind: 'carto')
layer.data_layer?.should == true
end
end
describe '#rename_table' do
it 'renames table in layer options' do
table_name = 'table_name'
new_table_name = 'changed_name'
tile_style = "##{table_name} { color:red; }"
query = "SELECT * FROM table_name, other_table"
options = {
table_name: table_name,
tile_style: tile_style,
query: query
}
layer = layer_class.create(kind: 'carto', options: options)
layer.rename_table(table_name, new_table_name)
layer.save
layer.reload
options = layer.options
options.fetch('tile_style') .should =~ /#{new_table_name}/
options.fetch('tile_style') .should_not =~ /#{table_name}/
options.fetch('query') .should =~ /#{new_table_name}/
options.fetch('query') .should_not =~ /#{table_name}/
options.fetch('table_name') .should =~ /#{new_table_name}/
options.fetch('table_name') .should_not =~ /#{table_name}/
end
it "won't touch the query if it doesn't match" do
table_name = 'table_name'
new_table_name = 'changed_name'
tile_style = "##{table_name} { color:red; }"
query = "SELECT * FROM foo"
options = {
table_name: table_name,
tile_style: tile_style,
query: query
}
layer = layer_class.create(kind: 'carto', options: options)
layer.rename_table(table_name, new_table_name)
layer.save
layer.reload
layer.options.fetch('query').should == options.fetch(:query)
end
end
describe '#destroy' do
it 'invalidates the vizjson cache of all related maps' do
map = create_map(user_id: @user.id, table_id: @table.id)
layer = layer_class.create(kind: 'carto')
add_layer_to_entity(map, layer)
# TODO: should be once
map.class.any_instance.expects(:notify_map_change).at_least_once
layer.destroy
end
it 'deletes ternary relations' do
map = create_map(user_id: @user.id, table_id: @table.id)
layer = layer_class.create(kind: 'carto')
layer.options[:query] = "SELECT * FROM #{@table.name}"
layer.register_table_dependencies
add_layer_to_entity(map, layer)
add_layer_to_entity(@user, layer)
layer.destroy
Carto::LayersMap.where(layer_id: layer.id).exists?.should be_false
Carto::LayersUser.where(layer_id: layer.id).exists?.should be_false
Carto::LayersUserTable.where(layer_id: layer.id).exists?.should be_false
end
end
describe '#uses_private_tables?' do
it 'returns true if any of the affected tables is private' do
layers = @table.table_visualization.data_layers
layers.length.should == 1
layers.first.uses_private_tables?.should be_true
@table.privacy = UserTable::PRIVACY_PUBLIC
@table.save
@user.reload
layers.first.reload
layers.first.uses_private_tables?.should be_false
end
end
context 'viewer role' do
after(:each) do
@user.viewer = false
@user.save
end
it "can't update layers" do
map = create_map(user_id: @user.id, table_id: @table.id)
layer = layer_class.create(kind: 'carto')
add_layer_to_entity(map, layer)
@user.viewer = true
@user.save
layer.reload
layer.kind = 'torque'
saved = begin
layer.save
rescue
false
end
saved.should be_false
end
end
describe '#LayerNodeStyle cache' do
before(:each) do
@map = create_map(user_id: @user.id, table_id: @table.id)
@layer = layer_class.create(kind: 'carto')
add_layer_to_entity(@map, @layer)
end
after(:each) do
@layer.destroy
@map.destroy
end
it 'saves styles for layers with source_id' do
@layer.options['source'] = 'a0'
@layer.save
lns = LayerNodeStyle.where(layer_id: @layer.id).first
lns.should be
lns.tooltip.should eq @layer.tooltip || {}
lns.infowindow.should eq @layer.infowindow || {}
lns.options['tile_style'].should eq @layer.options['tile_style']
lns.options['sql_wrap'].should eq @layer.options['sql_wrap']
lns.options['style_properties'].should eq @layer.options['style_properties']
end
it 'does not save styles for layers with source_id' do
@layer.options['source'] = nil
@layer.save
LayerNodeStyle.where(layer_id: @layer.id).count.should eq 0
end
it 'saves styles for torque layers' do
@layer.kind = 'torque'
@layer.options['source'] = 'a0'
@layer.save
lns = LayerNodeStyle.where(layer_id: @layer.id).first
lns.should be
lns.tooltip.should eq @layer.tooltip || {}
lns.infowindow.should eq @layer.infowindow || {}
lns.options['tile_style'].should eq @layer.options['tile_style']
lns.options['sql_wrap'].should eq @layer.options['sql_wrap']
lns.options['style_properties'].should eq @layer.options['style_properties']
end
end
end
+89
View File
@@ -0,0 +1,89 @@
require 'spec_helper'
require 'models/layer_shared_examples'
describe Layer do
it_behaves_like 'Layer model' do
let(:layer_class) { Layer }
def create_map(options = {})
Map.create(options)
end
def add_layer_to_entity(entity, layer)
entity.add_layer(layer)
end
before(:all) do
@quota_in_bytes = 500.megabytes
@table_quota = 500
@user = FactoryGirl.create(:valid_user, private_tables_enabled: true)
@table = Table.new
@table.user_id = @user.id
@table.save
end
before(:each) do
bypass_named_maps
end
after(:all) do
@user.destroy
end
describe '#copy' do
it 'returns a copy of the layer' do
layer = layer_class.create(kind: 'carto', options: { style: 'bogus' })
layer_copy = layer.copy
layer_copy.kind.should == layer.kind
layer_copy.options.should == layer.options
layer_copy.id.should be_nil
end
end
end
describe '#affected_table_names' do
include UniqueNamesHelper
before(:all) do
helper = TestUserFactory.new
@organization = FactoryGirl.create(:organization, quota_in_bytes: 1000000000000)
@owner = helper.create_owner(@organization)
@nonhyphen_user = helper.create_test_user(unique_name('user'), @organization)
@hyphen_user = helper.create_test_user(unique_name('user-'), @organization)
@nonhyphen_table = FactoryGirl.create(:user_table, user: @nonhyphen_user, name: unique_name('table'))
@subuser_table = FactoryGirl.create(:user_table, user: @hyphen_user, name: unique_name('table'))
@hyphen_table = FactoryGirl.create(:user_table, user: @hyphen_user, name: unique_name('table-'))
end
before(:each) do
@hyphen_user_layer = Layer.new
@hyphen_user_layer.stubs(:user).returns(@hyphen_user)
@nonhyphen_layer = Layer.new
@nonhyphen_layer.stubs(:user).returns(@nonhyphen_user)
end
it 'returns normal tables' do
@nonhyphen_layer.send(:affected_table_names, "SELECT * FROM #{@nonhyphen_table.name}")
.should eq ["#{@nonhyphen_user.username}.#{@nonhyphen_table.name}"]
end
it 'returns tables from users with hyphens' do
@hyphen_user_layer.send(:affected_table_names, "SELECT * FROM #{@subuser_table.name}")
.should eq ["\"#{@hyphen_user.username}\".#{@subuser_table.name}"]
end
it 'returns table with hyphens in the name' do
@hyphen_user_layer.send(:affected_table_names, "SELECT * FROM \"#{@hyphen_table.name}\"")
.should eq ["\"#{@hyphen_user.username}\".\"#{@hyphen_table.name}\""]
end
it 'returns multiple tables' do
@hyphen_user_layer.send(:affected_table_names, "SELECT * FROM \"#{@hyphen_table.name}\", #{@subuser_table.name}")
.should =~ ["\"#{@hyphen_user.username}\".\"#{@hyphen_table.name}\"",
"\"#{@hyphen_user.username}\".#{@subuser_table.name}"]
end
end
end
+235
View File
@@ -0,0 +1,235 @@
require_relative '../spec_helper'
include CartoDB
describe CartoDB::Log do
describe '#basic' do
it 'checks basic operations' do
user_id = UUIDTools::UUID.timestamp_create.to_s
type = Log::TYPE_DATA_IMPORT
text1 = 'test'
text2 = 'anothertest'
timestamp = Time.now.utc
log = Log.new({ type: type })
log.append(text1, false, timestamp)
log.append(text2, false, timestamp)
# Only stores now upon explicit change
log.store
log_id = log.id
log = Log.where(id:log_id).first
log.id.should eq log_id
log.type.should eq type
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::END_OF_LOG_MARK
end
end
describe '#circular buffer logic' do
it 'checks that buffer half works as expected' do
max_entries_per_half = 2
timestamp = Time.now.utc
text1 = "1"
text2 = "2"
text3 = "3"
text4 = "4"
text5 = "5"
text6 = "6"
Log.any_instance.stubs(:half_max_size).returns(max_entries_per_half)
log = Log.new({ type: Log::TYPE_DATA_IMPORT })
# Fixed half
log.append(text1, false, timestamp)
log.append(text2, false, timestamp)
# circular half
10.times do
log.append('to be deleted', false, timestamp)
end
log.append(text3, false, timestamp)
log.append(text4, false, timestamp)
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ]) +
Log::END_OF_LOG_MARK
log.append(text5, false, timestamp)
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text4 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text5 ]) +
Log::END_OF_LOG_MARK
log.append(text6, false, timestamp)
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text5 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text6 ]) +
Log::END_OF_LOG_MARK
end
it 'checks that loading a log with existing entries works' do
max_entries_per_half = 2
timestamp = Time.now.utc
text1 = "aaa"
text2 = "bbb"
text3 = "3.4"
text4 = "5 6 7 8"
text5 = "five"
text6 = "six"
Log.any_instance.stubs(:half_max_size).returns(max_entries_per_half)
log = Log.new({ type: Log::TYPE_DATA_IMPORT })
log.append(text1, false, timestamp)
log.append(text2, false, timestamp)
log.append(text3, false, timestamp)
log.append(text4, false, timestamp)
log.store
#reload
log = Log.where(id: log.id).first
# collect doesn't beautifies
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ])
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ]) +
Log::END_OF_LOG_MARK
# More tests
log = Log.new({ type: Log::TYPE_DATA_IMPORT })
log.append(text1, false, timestamp)
log.append(text2, false, timestamp)
log.append(text3, false, timestamp)
log.store
log = Log.where(id: log.id).first
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text3 ])
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
Log::END_OF_LOG_MARK
# Check that new entries are added correctly
log.append(text4, false, timestamp)
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ])
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ]) +
Log::END_OF_LOG_MARK
log.append(text5, false, timestamp)
log.append(text6, false, timestamp)
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [timestamp, text1]) +
(Log::ENTRY_FORMAT % [timestamp, text2]) +
(Log::ENTRY_FORMAT % [timestamp, text5]) +
(Log::ENTRY_FORMAT % [timestamp, text6])
log.to_s.should eq (Log::ENTRY_FORMAT % [timestamp, text1]) +
(Log::ENTRY_FORMAT % [timestamp, text2]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [timestamp, text5]) +
(Log::ENTRY_FORMAT % [timestamp, text6]) +
Log::END_OF_LOG_MARK
log = Log.new({ type: Log::TYPE_DATA_IMPORT })
log.append(text1, false, timestamp)
log.store
log = Log.where(id: log.id).first
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ])
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
Log::END_OF_LOG_MARK
# This test checks that old logs with more lines than accepted get truncated correctly
log = Log.new({ type: Log::TYPE_DATA_IMPORT })
log.append(text1, false, timestamp)
log.append(text2, false, timestamp)
log.append('filll', false, timestamp)
log.append('filll more', false, timestamp)
# This goes to the circular area
log.append('filll even more', false, timestamp)
log.append(text3, false, timestamp)
log.append(text4, false, timestamp)
log.store
log = Log.where(id: log.id).first
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ])
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ]) +
Log::END_OF_LOG_MARK
#Nothing should change with this
log.send(:fix_entries_encoding)
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ])
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text2 ]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [ timestamp, text3 ]) +
(Log::ENTRY_FORMAT % [ timestamp, text4 ]) +
Log::END_OF_LOG_MARK
# Check that new entries are added correctly
log.append(text5, false, timestamp)
log.append(text6, false, timestamp)
log.send(:collect_entries).should eq (Log::ENTRY_FORMAT % [timestamp, text1]) +
(Log::ENTRY_FORMAT % [timestamp, text2]) +
(Log::ENTRY_FORMAT % [timestamp, text5]) +
(Log::ENTRY_FORMAT % [timestamp, text6])
log.to_s.should eq (Log::ENTRY_FORMAT % [ timestamp, text1 ]) +
(Log::ENTRY_FORMAT % [timestamp, text2]) +
Log::HALF_OF_LOG_MARK +
(Log::ENTRY_FORMAT % [timestamp, text5]) +
(Log::ENTRY_FORMAT % [timestamp, text6]) +
Log::END_OF_LOG_MARK
end
it 'checks zero case of a new log' do
log = Log.new({ type: Log::TYPE_DATA_IMPORT })
log.to_s.should eq Log::END_OF_LOG_MARK
log = Log.where(id: log.id).first
log.to_s.should eq Log::END_OF_LOG_MARK
# Forcing call without entries
log.send(:fix_entries_encoding)
log.to_s.should eq Log::END_OF_LOG_MARK
end
end
end
+25
View File
@@ -0,0 +1,25 @@
require_relative '../../spec_helper'
require_relative '../../../app/models/map/copier'
require_relative '../../../app/models/layer'
describe CartoDB::Map::Copier do
before do
@user_id = UUIDTools::UUID.timestamp_create.to_s
@map = Map.new(user_id: @user_id)
@map.stubs(:layers).returns((1..5).map { Layer.new(kind: 'carto') })
@copier = CartoDB::Map::Copier.new
end
describe '#copy' do
it 'returns a copy of the original map' do
new_map = @copier.copy(@map)
new_map.should be_an_instance_of Map
new_map.user_id.should == @user_id
end
it 'copies all layers from the original map' do
new_map = @copier.copy(@map)
new_map.layers.length.should == 5
end
end #copy
end # CartoDB::Map::Copier
+579
View File
@@ -0,0 +1,579 @@
require_relative '../spec_helper'
require_relative '../../app/models/map'
require_relative '../../app/models/visualization/member'
require_dependency 'carto/bounding_box_utils'
describe Map do
before(:each) do
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
bypass_named_maps
@user = FactoryGirl.create(:valid_user, private_tables_enabled: true)
@table = create_table(user_id: @user.id)
end
after(:each) do
@table.destroy
@user.destroy
end
describe 'viewer role support' do
describe '#save' do
it 'should fail for viewer users' do
@user.stubs(:viewer).returns(true)
new_map = Map.new(user: @user, table_id: @table.id)
new_map.save.should eq nil
new_map.errors[:user].should eq ["Viewer users can't save maps"]
@user.stubs(:viewer).returns(false)
end
end
describe '#update' do
it 'should fail for existing maps and viewer users' do
new_map = Map.create(user_id: @user.id, table_id: @table.id)
new_map.user.stubs(:viewer).returns(true)
new_map.save.should eq nil
new_map.errors[:user].should eq ["Viewer users can't save maps"]
new_map.user.stubs(:viewer).returns(false)
new_map.destroy
end
end
describe '#validations' do
before(:all) do
@map_user = FactoryGirl.create(:carto_user)
@map = Carto::Map.create(user_id: @map_user.id)
end
after(:all) do
@map.destroy
@map_user.destroy
end
describe '#options' do
it 'sets dashboard_menu true by default' do
@map.dashboard_menu.should eq true
end
it 'sets layer_selector false by default' do
@map.layer_selector.should eq false
end
it 'allows to change dashboard_menu' do
@map.dashboard_menu = false
@map.dashboard_menu.should be_false
@map.dashboard_menu = true
@map.dashboard_menu.should be_true
end
it 'allows to change layer_selector' do
@map.layer_selector = false
@map.layer_selector.should be_false
@map.layer_selector = true
@map.layer_selector.should be_true
end
it 'rejects a non-boolean dashboard_menu value' do
@map.dashboard_menu = 'patata'
@map.valid?.should be_false
@map.errors[:options][0].should include('String did not match the following type: boolean')
end
it 'rejects a non-boolean layer_selector value' do
@map.layer_selector = 'patata'
@map.valid?.should be_false
@map.errors[:options][0].should include('String did not match the following type: boolean')
end
it 'requires a dashboard_menu value' do
@map.dashboard_menu = nil
@map.valid?.should be_false
@map.errors[:options].should_not be_empty
@map.errors[:options][0].should include('NilClass did not match the following type: boolean')
end
it 'requires a layer_selector value' do
@map.layer_selector = nil
@map.valid?.should be_false
@map.errors[:options].should_not be_empty
@map.errors[:options][0].should include('NilClass did not match the following type: boolean')
end
it 'requires dashboard_menu to be present' do
old_options = @map.options.dup
@map.options = Hash.new
@map.valid?.should be_false
@map.errors[:options].should_not be_empty
@map.errors[:options][0].should include('did not contain a required property of \'dashboard_menu\'')
@map.options = old_options
end
it 'requires layer_selector to be present' do
old_options = @map.options.dup
@map.options = Hash.new
@map.valid?.should be_false
@map.errors[:options].should_not be_empty
@map.errors[:options][0].should include('did not contain a required property of \'layer_selector\'')
@map.options = old_options
end
it 'rejects spammy options' do
@map.options[:spam] = 'hell'
@map.valid?.should be_false
@map.errors[:options].should_not be_empty
@map.errors[:options][0].should include('spam')
end
it 'rejects incomplete options' do
@map.options.delete(:dashboard_menu)
@map.valid?.should be_false
@map.errors[:options].should_not be_empty
@map.errors[:options][0].should include('dashboard_menu')
end
end
end
describe '#destroy' do
it 'should fail for existing maps and viewer users' do
new_map = Map.create(user_id: @user.id, table_id: @table.id)
new_map.user.stubs(:viewer).returns(true)
expect { new_map.destroy }.to raise_error(CartoDB::InvalidMember, /Viewer users can't destroy maps/)
new_map.user.stubs(:viewer).returns(false)
new_map.destroy
end
end
end
describe '#bounds' do
it 'checks max-min bounds' do
new_map = Map.create(user_id: @user.id, table_id: @table.id)
max_value = :maxx # 179
min_value = :minx # -179
value1 = 5
value2 = 179
value3 = -179
value4 = 180
value5 = -180
value6 = 0
Carto::BoundingBoxUtils.bound_for(value1, min_value, max_value).should eq value1
Carto::BoundingBoxUtils.bound_for(value2, min_value, max_value).should eq value2
Carto::BoundingBoxUtils.bound_for(value2, min_value, max_value).should eq Carto::BoundingBoxUtils::DEFAULT_BOUNDS[max_value]
Carto::BoundingBoxUtils.bound_for(value3, min_value, max_value).should eq value3
Carto::BoundingBoxUtils.bound_for(value3, min_value, max_value).should eq Carto::BoundingBoxUtils::DEFAULT_BOUNDS[min_value]
Carto::BoundingBoxUtils.bound_for(value4, min_value, max_value).should eq Carto::BoundingBoxUtils::DEFAULT_BOUNDS[max_value]
Carto::BoundingBoxUtils.bound_for(value5, min_value, max_value).should eq Carto::BoundingBoxUtils::DEFAULT_BOUNDS[min_value]
Carto::BoundingBoxUtils.bound_for(value6, min_value, max_value).should eq value6
# As map has no geometries, bounds should still be default ones instead of zeros
new_map.send(:get_map_bounds).should be_nil
new_map.destroy
end
end
describe '#tables' do
it 'returns the associated tables' do
map = Map.create(user_id: @user.id, table_id: @table.id)
@table.reload
map.reload
map.tables.map(&:id).should include(@table.id)
map.destroy
end
it 'updates associated tables/vis upon change' do
map = Map.create(user_id: @user.id, table_id: @table.id)
@table.reload
CartoDB::Visualization::Member.new(
privacy: CartoDB::Visualization::Member::PRIVACY_PUBLIC,
name: 'wadus',
type: CartoDB::Visualization::Member::TYPE_CANONICAL,
user_id: @user.id,
map_id: map.id
).store
map2 = Map.create(user_id: @user.id, table_id: @table.id)
# Change map_id on the table, but visualization still points to old map.id
@table.map_id = map2.id
@table.save
# Upon save of the original map, will sanitize all visualizations pointing to old one, saving with new one
CartoDB::Visualization::Member.any_instance.expects(:store_from_map).at_least_once
map.save
map.destroy
map2.destroy
end
end
describe '#base_layers' do
it 'returns the associated base layer' do
map = Map.create(user_id: @user.id, table_id: @table.id)
base_layer = Layer.create(kind: 'carto')
map.add_layer(base_layer)
5.times { map.add_layer(Layer.create(kind: 'carto')) }
map.reload.base_layers.first.id.should == base_layer.id
map.destroy
end
end
describe 'data_layers' do
it 'returns the associated data layers' do
map = Map.create(user_id: @user.id, table_id: @table.id)
5.times { map.add_layer(Layer.create(kind: 'tiled')) }
data_layer = Layer.create(kind: 'carto')
map.add_layer(data_layer)
map.reload.data_layers.first.id.should == data_layer.id
map.destroy
end
end
describe '#user_layers' do
it 'returns all user-defined layers' do
map = Map.create(user_id: @user.id, table_id: @table.id)
5.times { map.add_layer(Layer.create(kind: 'tiled')) }
data_layer = Layer.create(kind: 'carto')
map.add_layer(data_layer)
map.reload.base_layers.length.should == 6
map.reload.user_layers.length.should == 5
map.destroy
end
end
describe '#after_save' do
it 'invalidates varnish cache' do
map = ::Map[@table.map.id]
# One per save, one per destroy
map.expects(:invalidate_vizjson_varnish_cache).twice
map.save
map.destroy
end
it "recalculates bounds" do
table = Table.new :privacy => UserTable::PRIVACY_PRIVATE, :name => 'Madrid Bars', :tags => 'movies, personal'
table.user_id = @user.id
table.force_schema = "name text, address text, latitude float, longitude float"
table.save
table.insert_row!({:name => "Hawai", :address => "Calle de Pérez Galdós 9, Madrid, Spain", :latitude => 40.423012, :longitude => -3.699732})
table.insert_row!({:name => "El Estocolmo", :address => "Calle de la Palma 72, Madrid, Spain", :latitude => 40.426949, :longitude => -3.708969})
table.insert_row!({:name => "El Rey del Tallarín", :address => "Plaza Conde de Toreno 2, Madrid, Spain", :latitude => 40.424654, :longitude => -3.709570})
table.insert_row!({:name => "El Lacón", :address => "Manuel Fernández y González 8, Madrid, Spain", :latitude => 40.415113, :longitude => -3.699871})
table.insert_row!({:name => "El Pico", :address => "Calle Divino Pastor 12, Madrid, Spain", :latitude => 40.428198, :longitude => -3.703991})
table.reload
table.georeference_from!(:latitude_column => :latitude, :longitude_column => :longitude)
table.optimize
table.map.recalculate_bounds!
table.map.view_bounds_ne.should == "[40.4283, -3.69968]"
table.map.view_bounds_sw.should == "[40.415, -3.70962]"
end
it "recenters map using bounds" do
table = Table.new :privacy => UserTable::PRIVACY_PRIVATE, :name => 'bounds tests', :tags => 'testing'
table.user_id = @user.id
table.force_schema = "name text, latitude float, longitude float"
table.save
table.insert_row!({:name => "A", :latitude => 40.0, :longitude => -20.0})
table.insert_row!({:name => "B", :latitude => 80.0, :longitude => 30.0})
table.reload
table.georeference_from!(:latitude_column => :latitude, :longitude_column => :longitude)
table.optimize
table.map.set_default_boundaries!
# casting to string :_( but currently only used by frontend
table.map.center_data.should == [ 60.0.to_s, 5.0.to_s ]
end
it "recalculates zoom using bounds" do
table = Table.new :privacy => UserTable::PRIVACY_PRIVATE, :name => 'zoom recalc test'
table.user_id = @user.id
table.force_schema = "name text, latitude float, longitude float"
table.save
# Out of usual bounds by being bigger than "full world bounding box"
table.map.stubs(:get_map_bounds)
.returns({ minx: -379, maxx: 379, miny: -285 , maxy: 285.0511})
table.map.set_default_boundaries!
table.map.zoom.should == 1
table.map.stubs(:get_map_bounds)
.returns({ minx: -179, maxx: 179, miny: -85 , maxy: 85.0511})
table.map.set_default_boundaries!
table.map.zoom.should == 1
table.map.stubs(:get_map_bounds)
.returns({ minx: 1, maxx: 2, miny: 1 , maxy: 2})
table.map.set_default_boundaries!
table.map.zoom.should == 8
table.map.stubs(:get_map_bounds)
.returns({ minx: 0.025, maxx: 0.05, miny: 0.025 , maxy: 0.05})
table.map.set_default_boundaries!
table.map.zoom.should == 14
# Smaller than our max zoom level
table.map.stubs(:get_map_bounds)
.returns({ minx: 0.000001, maxx: 0.000002, miny: 0.000001 , maxy: 0.000002})
table.map.set_default_boundaries!
table.map.zoom.should == 18
end
end
describe '#notify_map_change' do
before(:each) do
@map = Map.create(user_id: @user.id, table_id: @table.id)
layer = Layer.new(kind: 'tiled')
@map.add_layer(layer)
end
after(:each) do
@map.destroy
end
it 'invalidates vizjson cache' do
CartoDB::Varnish.any_instance.stubs(:purge).with(@map.visualization.varnish_vizjson_key).once
@map.notify_map_change
CartoDB::Varnish.any_instance.stubs(:purge) # Needed to avoid counting the call from destroy
end
it 'updates_named_maps' do
named_maps_api_mock = mock
Carto::NamedMaps::Api.stubs(:new).with { |vis| vis.id == @map.visualization.id }.returns(named_maps_api_mock)
named_maps_api_mock.stubs(:show).returns(true)
named_maps_api_mock.stubs(:update).once
@map.notify_map_change
Carto::NamedMaps::Api.unstub(:new)
end
end
describe '#updated_at' do
it 'is updated after saving the map' do
map = Map.create(user_id: @user.id, table_id: @table.id)
updated_at = map.updated_at
sleep 0.5
map.save
map.updated_at.should > updated_at
map.destroy
end
end
describe '#can_add_layer?' do
it 'should only take into account data layers' do
map = Map.create(user_id: @user.id, table_id: @table.id)
@user.max_layers = 1
@user.save.reload
# First base layer is always allowed
layer = Layer.new(kind: 'tiled')
map.can_add_layer?(@user, layer).should == true
map.add_layer(layer)
map.save.reload
layer_carto1 = Layer.new(kind: 'carto')
map.can_add_layer?(@user, layer_carto1).should == true
map.add_layer(layer_carto1)
map.save.reload
layer_carto2 = Layer.new(kind: 'carto')
map.can_add_layer?(@user, layer_carto2).should == false
# This is now a valid scenario, for example switcing from a basemap with labels on top to another that has too
third_layer = Layer.new(kind: 'tiled', order: 15)
map.can_add_layer?(@user, third_layer).should == true
map.add_layer(third_layer)
map.save.reload
map.destroy
end
end
describe '#admits?' do
it 'checks base layer admission rules' do
map = Map.create(user_id: @user.id, table_id: @table.id)
# First base layer is always allowed
layer = Layer.new(kind: 'tiled')
map.admits_layer?(layer).should == true
map.add_layer(layer)
map.save.reload
map.add_layer(Layer.new(kind: 'carto', order: 5))
map.save.reload
second_tiled_layer = Layer.new(kind: 'tiled')
# more tiled layers allowed only if at top
second_tiled_layer.order = 0
map.admits_layer?(second_tiled_layer).should == false
second_tiled_layer.order = 15
map.admits_layer?(second_tiled_layer).should == true
map.add_layer(second_tiled_layer)
map.save.reload
# This is now a valid scenario, for example switcing from a basemap with labels on top to another that has too
third_layer = Layer.new(kind: 'tiled', order: 15)
map.admits_layer?(third_layer).should == true
third_layer.order = 100
map.admits_layer?(third_layer).should == true
map.destroy
end
describe 'when linked to a table visualization' do
it 'returns false when passed a data layer and it is already linked to a base layer' do
map = ::Map[@table.map.id]
map.remove_layer(map.data_layers.first)
map.reload
map.admits_layer?(Layer.new(kind: 'carto')).should == true
map.add_layer(Layer.new(kind: 'carto'))
map.save.reload
map.admits_layer?(Layer.new(kind: 'carto')).should == false
end
end
end
it "should correcly set vizjson updated_at" do
map = Map.create(user_id: @user.id, table_id: @table.id)
# When the table data is newer
time = Time.now + 2.minutes
Table.any_instance.stubs(:data_last_modified).returns(time)
map.viz_updated_at.to_s.should == time.to_s
# When the data layer is newer
time = Time.now + 3.minutes
map.stubs(:data_layers).returns([Layer.new(updated_at: time)])
map.viz_updated_at.to_s.should == time.to_s
end
describe '#before_destroy' do
it 'invalidates varnish cache' do
map = ::Map[@table.map.id]
map.expects(:invalidate_vizjson_varnish_cache)
map.destroy
end
end
describe '#process_privacy_in' do
it 'sets related visualization private if layer uses private tables' do
@table1 = Table.new
@table1.user_id = @user.id
@table1.save
@table2 = Table.new
@table2.user_id = @user.id
@table2.save
visualization = @table1.table_visualization
visualization.data_layers.length.should eq 1
@table1.privacy = UserTable::PRIVACY_PUBLIC
@table1.save
visualization.privacy = CartoDB::Visualization::Member::PRIVACY_PUBLIC
visualization.store
visualization.private?.should be_false
layer = Carto::Layer.create(
kind: 'carto',
options: { table_name: @table2.name }
)
layer.add_map(visualization.map)
layer.save
layer.reload
@user.reload
layer.uses_private_tables?.should be_true
visualization.map.process_privacy_in(layer)
visualization.private?.should be_true
end
end
context 'viewer role support on layer management' do
after(:each) do
@user.viewer = false
@user.save
@user.reload
@map.reload && @map.destroy if @map
end
def become_viewer(user)
user.viewer = true
user.save
user.reload
end
describe 'add layers' do
it 'should fail for viewer users' do
@map = Map.create(user_id: @user.id, table_id: @table.id)
become_viewer(@user)
@map.reload
@layer = Layer.create(kind: 'carto', options: { query: "select * from #{@table.name}" })
expect { @map.add_layer(@layer) }.to raise_error(/Viewer users can't edit layers/)
end
end
describe 'remove layers' do
it 'should fail for viewer users' do
@map = Map.create(user_id: @user.id, table_id: @table.id)
layer = Layer.create(kind: 'carto', options: { table_name: @table.name })
layer.add_map(@map)
layer.save
layer.reload
@map.reload
@map.layers_dataset.where(layer_id: layer.id).should_not be_empty
become_viewer(@user)
expect {
@map.layers_dataset.where(layer_id: layer.id).destroy
}.to raise_error(/Viewer users can't destroy layers/)
@map.reload
@map.layers_dataset.where(layer_id: layer.id).should_not be_empty
end
end
describe 'can_add_layer?' do
it 'should return false for viewer users' do
@map = Map.create(user_id: @user.id, table_id: @table.id)
become_viewer(@user)
@map.reload
layer = Layer.create(kind: 'carto', options: { table_name: @table.name })
@map.can_add_layer?(@user, layer).should eq false
end
end
end
end
+114
View File
@@ -0,0 +1,114 @@
require_relative '../spec_helper'
# Tests should define the following methods:
# - get_organization: returns a correspoding Organization instance
# - get_twitter_imports_count_by_organization_id: returns organization import count. Needed because implementations don't share a common interface
shared_examples_for "organization models" do
include_context 'users helper'
include_context 'organization with users helper'
describe "#get_geocoding_calls" do
it "counts all geocodings within the org" do
base_line = get_geocoding_calls_by_organization_id(@organization.id)
get_organization.owner.geocoder_provider = 'heremaps'
org_user_1_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(
@org_user_1.username,
@org_user_1.organization.name
)
org_user_1_geocoder_metrics.incr(:geocoder_here, :success_responses, 2)
org_user_1_geocoder_metrics.incr(:geocoder_cache, :success_responses, 3)
org_user_2_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(
@org_user_2.username,
@org_user_2.organization.name
)
org_user_2_geocoder_metrics.incr(:geocoder_here, :success_responses, 4)
org_user_2_geocoder_metrics.incr(:geocoder_cache, :success_responses, 5)
user1_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(@user1.username, nil)
user1_geocoder_metrics.incr(:geocoder_here, :success_responses, 2)
user1_geocoder_metrics.incr(:geocoder_cache, :success_responses, 3)
::User.any_instance.expects(:get_geocoding_calls).never
get_geocoding_calls_by_organization_id(@organization.id).should == base_line + 14
end
it "counts all geocodings within the org using the org provider" do
base_line = get_geocoding_calls_by_organization_id(@organization.id)
get_organization.owner.geocoder_provider = 'tomtom'
org_user_1_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(
@org_user_1.username,
@org_user_1.organization.name
)
org_user_1_geocoder_metrics.incr(:geocoder_here, :success_responses, 2)
org_user_1_geocoder_metrics.incr(:geocoder_cache, :success_responses, 3)
org_user_2_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(
@org_user_2.username,
@org_user_2.organization.name
)
org_user_2_geocoder_metrics.incr(:geocoder_here, :success_responses, 4)
org_user_2_geocoder_metrics.incr(:geocoder_cache, :success_responses, 5)
user1_geocoder_metrics = CartoDB::GeocoderUsageMetrics.new(@user1.username, nil)
user1_geocoder_metrics.incr(:geocoder_here, :success_responses, 2)
user1_geocoder_metrics.incr(:geocoder_cache, :success_responses, 3)
::User.any_instance.expects(:get_geocoding_calls).never
get_geocoding_calls_by_organization_id(get_organization.id).should == base_line + 14
end
end
describe "#get_twitter_imports_count" do
it "counts all users twitter imports in a single query" do
FactoryGirl.create(:search_tweet, user: @org_user_1, retrieved_items: 5)
FactoryGirl.create(:search_tweet, user: @org_user_2, retrieved_items: 6)
FactoryGirl.create(:search_tweet, user: @user1, retrieved_items: 5)
# TODO: range and model expectation is needed because Sequel model is not counting sql queries, and can't know why.
::User.any_instance.expects(:get_twitter_imports_count).never
expect {
get_twitter_imports_count_by_organization_id(@organization.id).should == 11
}.to make_database_queries(count: 0..1)
end
end
describe "#signup_page_enabled" do
it 'is true if domain whitelist is not empty' do
organization = get_organization
organization.auth_username_password_enabled = true
organization.whitelisted_email_domains = ['carto.com']
organization.signup_page_enabled.should == true
end
it 'is false if domain whitelist is empty' do
organization = get_organization
organization.auth_username_password_enabled = true
organization.whitelisted_email_domains = []
organization.signup_page_enabled.should == false
end
it 'is false if no authentication is enabled' do
organization = get_organization
organization.auth_username_password_enabled = false
organization.auth_google_enabled = false
organization.auth_github_enabled = false
organization.whitelisted_email_domains = ['carto.com']
organization.signup_page_enabled.should be_false
end
end
it 'generates auth_tokens and save them for future accesses' do
token = get_organization.get_auth_token
token.should be
get_organization.reload
get_organization.get_auth_token.should eq token
end
end
+693
View File
@@ -0,0 +1,693 @@
require_relative '../spec_helper'
require_relative '../../app/models/visualization/collection'
require_relative '../../app/models/organization.rb'
require_relative 'organization_shared_examples'
require_relative '../factories/visualization_creation_helpers'
require 'helpers/account_types_helper'
require 'helpers/unique_names_helper'
require 'helpers/storage_helper'
require 'factories/organizations_contexts'
include CartoDB, StorageHelper, UniqueNamesHelper
describe 'refactored behaviour' do
it_behaves_like 'organization models' do
before(:each) do
@the_organization = ::Organization.where(id: @organization.id).first
end
def get_twitter_imports_count_by_organization_id(organization_id)
raise "id doesn't match" unless organization_id == @the_organization.id
@the_organization.get_twitter_imports_count
end
def get_geocoding_calls_by_organization_id(organization_id)
raise "id doesn't match" unless organization_id == @the_organization.id
@the_organization.get_geocoding_calls
end
def get_organization
@the_organization
end
end
end
describe Organization do
before(:all) do
@user = create_user(:quota_in_bytes => 524288000, :table_quota => 500)
end
after(:all) do
bypass_named_maps
begin
@user.destroy
rescue
# Silence error, can't do much more
end
end
before(:each) do
create_account_type_fg('ORGANIZATION USER')
end
describe '#destroy_cascade' do
include TableSharing
before(:each) do
@organization = FactoryGirl.create(:organization)
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
end
after(:each) do
@organization.delete if @organization.try(:persisted?)
end
it 'Destroys users and owner as well' do
organization = Organization.new(quota_in_bytes: 123456789000, name: 'wadus', seats: 5).save
owner = create_user(:quota_in_bytes => 524288000, :table_quota => 500)
owner_org = CartoDB::UserOrganization.new(organization.id, owner.id)
owner_org.promote_user_to_admin
owner.reload
organization.reload
user = create_user(quota_in_bytes: 524288000, table_quota: 500, organization_id: organization.id)
user.save
user.reload
organization.reload
organization.users.count.should eq 2
organization.destroy_cascade
Organization.where(id: organization.id).first.should be nil
::User.where(id: user.id).first.should be nil
::User.where(id: owner.id).first.should be nil
end
it 'Destroys viewer users with shared visualizations' do
organization = Organization.new(quota_in_bytes: 123456789000, name: 'wadus', seats: 3, viewer_seats: 2).save
owner = create_user(quota_in_bytes: 524288000, table_quota: 500)
owner_org = CartoDB::UserOrganization.new(organization.id, owner.id)
owner_org.promote_user_to_admin
user1 = create_user(organization_id: organization.id)
user2 = create_user(organization_id: organization.id)
table1 = create_table(user_id: user1.id)
table2 = create_table(user_id: user2.id)
share_table_with_user(table1, user2)
share_table_with_user(table2, user1)
user1.viewer = true
user1.save
user2.viewer = true
user2.save
organization.destroy_cascade
Organization.where(id: organization.id).first.should be nil
::User.where(id: user1.id).first.should be nil
::User.where(id: user2.id).first.should be nil
::User.where(id: owner.id).first.should be nil
Carto::UserTable.exists?(table1.id).should be_false
Carto::UserTable.exists?(table2.id).should be_false
end
it 'destroys users with unregistered tables' do
organization = Organization.new(quota_in_bytes: 123456789000, name: 'wadus', seats: 5).save
owner = create_user(quota_in_bytes: 524288000, table_quota: 500)
owner_org = CartoDB::UserOrganization.new(organization.id, owner.id)
owner_org.promote_user_to_admin
owner.reload
organization.reload
user = create_user(quota_in_bytes: 524288000, table_quota: 500, organization_id: organization.id)
user.save
user.reload
organization.reload
user.in_database.run('CREATE TABLE foobarbaz (id serial)')
organization.users.count.should eq 2
organization.destroy_cascade
Organization.where(id: organization.id).first.should be nil
::User.where(id: user.id).first.should be nil
::User.where(id: owner.id).first.should be nil
end
it 'destroys its groups through the extension' do
Carto::Group.any_instance.expects(:destroy_group_with_extension).once
FactoryGirl.create(:carto_group, organization: Carto::Organization.find(@organization.id))
@organization.destroy
end
it 'destroys assets' do
bypass_storage
asset = FactoryGirl.create(:organization_asset,
organization_id: @organization.id)
@organization.destroy
Carto::Asset.exists?(asset.id).should be_false
end
it 'calls :delete_in_central if delete_in_central parameter is true' do
pending "Don't implemented. See Organization#destroy_cascade"
@organization.expects(:delete_in_central).once
@organization.destroy_cascade(delete_in_central: true)
end
end
describe '#add_user_to_org' do
it 'Tests adding a user to an organization (but no owner)' do
org_quota = 123456789000
org_name = unique_name('org')
org_seats = 5
username = @user.username
organization = Organization.new
organization.name = org_name
organization.quota_in_bytes = org_quota
organization.seats = org_seats
organization.save
organization.valid?.should eq true
organization.errors.should eq Hash.new
@user.organization = organization
@user.save
user = ::User.where(username: username).first
user.should_not be nil
user.organization_id.should_not eq nil
user.organization_id.should eq organization.id
user.organization.should_not eq nil
user.organization.id.should eq organization.id
user.organization.name.should eq org_name
user.organization.quota_in_bytes.should eq org_quota
user.organization.seats.should eq org_seats
@user.organization = nil
@user.save
organization.destroy
end
it 'validates viewer and builder quotas' do
quota = 123456789000
name = unique_name('org')
seats = 1
viewer_seats = 1
organization = Organization.new(name: name, quota_in_bytes: quota, seats: seats, viewer_seats: viewer_seats).save
user = create_validated_user
CartoDB::UserOrganization.new(organization.id, user.id).promote_user_to_admin
organization.reload
user.reload
organization.remaining_seats.should eq 0
organization.remaining_viewer_seats.should eq 1
organization.users.should include(user)
viewer = create_validated_user(organization: organization, viewer: true)
viewer.valid? should be_true
viewer.reload
organization.reload
organization.remaining_seats.should eq 0
organization.remaining_viewer_seats.should eq 0
organization.users.should include(viewer)
builder = create_validated_user(organization: organization, viewer: false)
organization.reload
organization.remaining_seats.should eq 0
organization.remaining_viewer_seats.should eq 0
organization.users.should_not include(builder)
viewer2 = create_validated_user(organization: organization, viewer: true)
organization.reload
organization.remaining_seats.should eq 0
organization.remaining_viewer_seats.should eq 0
organization.users.should_not include(viewer2)
organization.seats = 0
organization.viewer_seats = 0
organization.valid?.should be_false
organization.errors.should include :seats, :viewer_seats
organization.destroy_cascade
end
it 'allows saving user if organization has no seats left' do
organization = FactoryGirl.create(:organization, seats: 2, viewer_seats: 0, quota_in_bytes: 10)
user = create_validated_user(quota_in_bytes: 1)
CartoDB::UserOrganization.new(organization.id, user.id).promote_user_to_admin
organization.reload
user.reload
builder = create_validated_user(organization: organization, viewer: false, quota_in_bytes: 2)
builder.organization.reload
builder.set_fields({ quota_in_bytes: 1 }, [:quota_in_bytes])
builder.save(raise_on_failure: true)
builder.reload
builder.quota_in_bytes.should eq 1
organization.destroy_cascade
end
it 'Tests setting a user as the organization owner' do
org_name = unique_name('org')
organization = Organization.new(quota_in_bytes: 123456789000, name: org_name, seats: 5).save
user = create_user(:quota_in_bytes => 524288000, :table_quota => 500)
user_org = CartoDB::UserOrganization.new(organization.id, user.id)
# This also covers the usecase of an user being moved to its own schema (without org)
user_org.promote_user_to_admin
organization.reload
user.reload
user.organization.id.should eq organization.id
user.organization.owner.id.should eq user.id
user.database_schema.should eq user.username
user_org = CartoDB::UserOrganization.new(organization.id, user.id)
expect {
user_org.promote_user_to_admin
}.to raise_error
user.destroy
end
end
describe '#org_members_and_owner_removal' do
it 'Tests removing a normal member from the organization' do
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
org_name = unique_name('org')
organization = Organization.new(quota_in_bytes: 123456789000, name: org_name, seats: 5).save
owner = create_user(:quota_in_bytes => 524288000, :table_quota => 500)
user_org = CartoDB::UserOrganization.new(organization.id, owner.id)
user_org.promote_user_to_admin
organization.reload
owner.reload
member1 = create_user(:quota_in_bytes => 524288000, :table_quota => 500, organization_id: organization.id)
member1.reload
organization.reload
member2 = create_user(:quota_in_bytes => 524288000, :table_quota => 500, organization_id: organization.id)
member2.reload
organization.users.count.should eq 3
results = member1.in_database(as: :public_user).fetch(%Q{
SELECT has_function_privilege('#{member1.database_public_username}', 'CDB_QueryTablesText(text)', 'execute')
}).first
results.nil?.should eq false
results[:has_function_privilege].should eq true
member1.destroy
organization.reload
organization.users.count.should eq 2
results = member2.in_database(as: :public_user).fetch(%Q{
SELECT has_function_privilege('#{member2.database_public_username}', 'CDB_QueryTablesText(text)', 'execute')
}).first
results.nil?.should eq false
results[:has_function_privilege].should eq true
# Can't remove owner if other members exist
expect {
owner.destroy
}.to raise_error CartoDB::BaseCartoDBError
member2.destroy
organization.reload
organization.users.count.should eq 1
results = owner.in_database(as: :public_user).fetch(%Q{
SELECT has_function_privilege('#{owner.database_public_username}', 'CDB_QueryTablesText(text)', 'execute')
}).first
results.nil?.should eq false
results[:has_function_privilege].should eq true
owner.destroy
expect {
organization.reload
}.to raise_error Sequel::Error
end
it 'Tests removing a normal member with analysis tables' do
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
org_name = unique_name('org')
organization = Organization.new(quota_in_bytes: 123456789000, name: org_name, seats: 5).save
owner = create_test_user('orgowner')
user_org = CartoDB::UserOrganization.new(organization.id, owner.id)
user_org.promote_user_to_admin
organization.reload
owner.reload
member1 = create_test_user('member1', organization)
create_random_table(member1, 'analysis_user_table')
create_random_table(member1, 'users_table')
member1.in_database.run(%{CREATE TABLE #{member1.database_schema}.analysis_4bd65e58e4_246c4acb2c67e4f3330d76c4be7c6deb8e07f344 (id serial)})
member1.reload
organization.reload
organization.users.count.should eq 2
results = member1.in_database(as: :public_user).fetch(%{
SELECT has_function_privilege('#{member1.database_public_username}', 'CDB_QueryTablesText(text)', 'execute')
}).first
results.nil?.should eq false
results[:has_function_privilege].should eq true
member1.destroy
organization.reload
organization.users.count.should eq 1
end
end
describe '#non_org_user_removal' do
it 'Tests removing a normal user' do
initial_count = ::User.all.count
user = create_user(:quota_in_bytes => 524288000, :table_quota => 50)
::User.all.count.should eq (initial_count + 1)
user.destroy
::User.all.count.should eq initial_count
::User.all.collect(&:id).should_not include(user.id)
end
end
describe '#users_in_same_db_removal_error' do
it "Tests that if 2+ users somehow have same database name, can't be deleted" do
user2 = create_user(:quota_in_bytes => 524288000, :table_quota => 50, :database_name => @user.database_name)
user2.database_name = @user.database_name
user2.save
expect {
user2.destroy
}.to raise_error CartoDB::BaseCartoDBError
end
end
describe '#unique_name' do
it 'Tests uniqueness of name' do
org_name = unique_name('org')
organization = Organization.new
organization.name = org_name
organization.quota_in_bytes = 123
organization.seats = 1
organization.errors
organization.valid?.should eq true
# Repeated username
organization.name = @user.username
organization.valid?.should eq false
organization.name = org_name
organization.save
organization2 = Organization.new
# Repeated name
organization2.name = org_name
organization2.quota_in_bytes = 123
organization2.seats = 1
organization2.valid?.should eq false
organization.destroy
end
end
describe "#get_api_calls and #get_geocodings" do
before(:each) do
@organization = create_organization_with_users(name: 'overquota-org')
end
after(:each) do
@organization.destroy
end
it "should return the sum of the api_calls for all organization users" do
::User.any_instance.stubs(:get_api_calls).returns (0..30).to_a
@organization.get_api_calls.should == (0..30).to_a.sum * @organization.users.size
end
end
describe '.overquota', focus: true do
before(:all) do
@organization = create_organization_with_users(name: 'overquota-org')
@owner = User.where(id: @organization.owner_id).first
end
after(:all) do
@organization.destroy
end
it "should return organizations over their geocoding quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.overquota.should be_empty
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(10)
Organization.any_instance.stubs(:get_geocoding_calls).returns 30
Organization.any_instance.stubs(:geocoding_quota).returns 10
Organization.overquota.map(&:id).should include(@organization.id)
Organization.overquota.size.should == Organization.count
end
it "should return organizations over their here isolines quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.overquota.should be_empty
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(10)
Organization.any_instance.stubs(:get_geocoding_calls).returns 0
Organization.any_instance.stubs(:geocoding_quota).returns 10
Organization.any_instance.stubs(:get_here_isolines_calls).returns 30
Organization.any_instance.stubs(:here_isolines_quota).returns 10
Organization.overquota.map(&:id).should include(@organization.id)
Organization.overquota.size.should == Organization.count
end
it "should return organizations over their data observatory snapshot quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.overquota.should be_empty
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(10)
Organization.any_instance.stubs(:get_geocoding_calls).returns 0
Organization.any_instance.stubs(:geocoding_quota).returns 10
Organization.any_instance.stubs(:get_obs_snapshot_calls).returns 30
Organization.any_instance.stubs(:obs_snapshot_quota).returns 10
Organization.overquota.map(&:id).should include(@organization.id)
Organization.overquota.size.should == Organization.count
end
it "should return organizations over their data observatory general quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.overquota.should be_empty
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(10)
Organization.any_instance.stubs(:get_geocoding_calls).returns 0
Organization.any_instance.stubs(:geocoding_quota).returns 10
Organization.any_instance.stubs(:get_obs_snapshot_calls).returns 0
Organization.any_instance.stubs(:obs_snapshot_quota).returns 10
Organization.any_instance.stubs(:get_obs_general_calls).returns 30
Organization.any_instance.stubs(:obs_general_quota).returns 10
Organization.overquota.map(&:id).should include(@organization.id)
Organization.overquota.size.should == Organization.count
end
it "should return organizations near their geocoding quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(120)
Organization.any_instance.stubs(:get_geocoding_calls).returns(81)
Organization.any_instance.stubs(:geocoding_quota).returns(100)
Organization.overquota.should be_empty
Organization.overquota(0.20).map(&:id).should include(@organization.id)
Organization.overquota(0.20).size.should == Organization.count
Organization.overquota(0.10).should be_empty
end
it "should return organizations near their here isolines quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(120)
Organization.any_instance.stubs(:get_geocoding_calls).returns(0)
Organization.any_instance.stubs(:geocoding_quota).returns(100)
Organization.any_instance.stubs(:get_here_isolines_calls).returns(81)
Organization.any_instance.stubs(:here_isolines_quota).returns(100)
Organization.any_instance.stubs(:get_obs_snapshot_calls).returns(0)
Organization.any_instance.stubs(:obs_snapshot_quota).returns(100)
Organization.any_instance.stubs(:get_obs_general_calls).returns(0)
Organization.any_instance.stubs(:obs_general_quota).returns(100)
Organization.any_instance.stubs(:get_mapzen_routing_calls).returns(81)
Organization.any_instance.stubs(:mapzen_routing_quota).returns(100)
Organization.overquota.should be_empty
Organization.overquota(0.20).map(&:id).should include(@organization.id)
Organization.overquota(0.20).size.should == Organization.count
Organization.overquota(0.10).should be_empty
end
it "should return organizations near their data observatory snapshot quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(120)
Organization.any_instance.stubs(:get_geocoding_calls).returns(0)
Organization.any_instance.stubs(:geocoding_quota).returns(100)
Organization.any_instance.stubs(:get_here_isolines_calls).returns(0)
Organization.any_instance.stubs(:here_isolines_quota).returns(100)
Organization.any_instance.stubs(:get_obs_general_calls).returns(0)
Organization.any_instance.stubs(:obs_general_quota).returns(100)
Organization.any_instance.stubs(:get_obs_snapshot_calls).returns(81)
Organization.any_instance.stubs(:obs_snapshot_quota).returns(100)
Organization.any_instance.stubs(:get_mapzen_routing_calls).returns(0)
Organization.any_instance.stubs(:mapzen_routing_quota).returns(100)
Organization.overquota.should be_empty
Organization.overquota(0.20).map(&:id).should include(@organization.id)
Organization.overquota(0.20).size.should == Organization.count
Organization.overquota(0.10).should be_empty
end
it "should return organizations near their data observatory general quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(120)
Organization.any_instance.stubs(:get_geocoding_calls).returns(0)
Organization.any_instance.stubs(:geocoding_quota).returns(100)
Organization.any_instance.stubs(:get_here_isolines_calls).returns(0)
Organization.any_instance.stubs(:here_isolines_quota).returns(100)
Organization.any_instance.stubs(:get_obs_snapshot_calls).returns(0)
Organization.any_instance.stubs(:obs_snapshot_quota).returns(100)
Organization.any_instance.stubs(:get_obs_general_calls).returns(81)
Organization.any_instance.stubs(:obs_general_quota).returns(100)
Organization.any_instance.stubs(:get_mapzen_routing_calls).returns(0)
Organization.any_instance.stubs(:mapzen_routing_quota).returns(100)
Organization.overquota.should be_empty
Organization.overquota(0.20).map(&:id).should include(@organization.id)
Organization.overquota(0.20).size.should == Organization.count
Organization.overquota(0.10).should be_empty
end
it "should return organizations over their mapzen routing quota" do
Organization.any_instance.stubs(:owner).returns(@owner)
Organization.overquota.should be_empty
Organization.any_instance.stubs(:get_api_calls).returns(0)
Organization.any_instance.stubs(:map_view_quota).returns(10)
Organization.any_instance.stubs(:get_geocoding_calls).returns 0
Organization.any_instance.stubs(:geocoding_quota).returns 10
Organization.any_instance.stubs(:get_here_isolines_calls).returns(0)
Organization.any_instance.stubs(:here_isolines_quota).returns(100)
Organization.any_instance.stubs(:get_mapzen_routing_calls).returns 30
Organization.any_instance.stubs(:mapzen_routing_quota).returns 10
Organization.overquota.map(&:id).should include(@organization.id)
Organization.overquota.size.should == Organization.count
end
end
it 'should validate password_expiration_in_d' do
organization = FactoryGirl.create(:organization)
organization.valid?.should be_true
organization.password_expiration_in_d.should_not be
# minimum 1 day
organization = FactoryGirl.create(:organization, password_expiration_in_d: 1)
organization.valid?.should be_true
organization.password_expiration_in_d.should eq 1
expect {
organization = FactoryGirl.create(:organization, password_expiration_in_d: 0)
}.to raise_error(Sequel::ValidationFailed, /password_expiration_in_d must be greater than 0 and lower than 366/)
# maximum 1 year
organization = FactoryGirl.create(:organization, password_expiration_in_d: 365)
organization.valid?.should be_true
organization.password_expiration_in_d.should eq 365
expect {
organization = FactoryGirl.create(:organization, password_expiration_in_d: 366)
}.to raise_error(Sequel::ValidationFailed, /password_expiration_in_d must be greater than 0 and lower than 366/)
# nil or blank means unlimited
organization = FactoryGirl.create(:organization, password_expiration_in_d: nil)
organization.valid?.should be_true
organization.password_expiration_in_d.should_not be
organization = FactoryGirl.create(:organization, password_expiration_in_d: '')
organization.valid?.should be_true
organization.password_expiration_in_d.should_not be
# defaults to global config if no value
organization = FactoryGirl.build(:organization, password_expiration_in_d: 1)
organization.valid?.should be_true
organization.save
organization = Carto::Organization.find(organization.id)
organization.valid?.should be_true
organization.password_expiration_in_d.should eq 1
organization.password_expiration_in_d = nil
organization.valid?.should be_true
organization.save
organization = Carto::Organization.find(organization.id)
organization.password_expiration_in_d.should_not be
# override default config if a value is set
organization = FactoryGirl.create(:organization, password_expiration_in_d: 10)
organization.valid?.should be_true
organization.password_expiration_in_d.should eq 10
# keep values configured
organization = Carto::Organization.find(organization.id)
organization.valid?.should be_true
organization.password_expiration_in_d.should eq 10
end
it 'should handle redis keys properly' do
@organization = create_organization_with_users(name: 'overquota-org')
$users_metadata.hkeys(@organization.key).should_not be_empty
@organization.destroy
$users_metadata.hkeys(@organization.key).should be_empty
end
def random_attributes(attributes = {})
random = unique_name('viz')
{
name: attributes.fetch(:name, random),
description: attributes.fetch(:description, "description #{random}"),
privacy: attributes.fetch(:privacy, Visualization::Member::PRIVACY_PUBLIC),
tags: attributes.fetch(:tags, ['tag 1']),
type: attributes.fetch(:type, Visualization::Member::TYPE_DERIVED),
user_id: attributes.fetch(:user_id, UUIDTools::UUID.timestamp_create.to_s)
}
end # random_attributes
end
+30
View File
@@ -0,0 +1,30 @@
require 'spec_helper'
require 'models/permissions_shared_examples'
describe CartoDB::Permission do
it_behaves_like 'permission models' do
def permission_from_visualization_id(entity_id)
visualization_from_id(entity_id).permission
end
def visualization_from_id(entity_id)
::Visualization::Member.new(id: entity_id).fetch
end
def permission_klass
::Permission
end
def user_for_association_from_id(user_id)
::User[user_id]
end
def save_permission(permission)
permission.save
end
def validation_error_klass
Sequel::ValidationFailed
end
end
end
+781
View File
@@ -0,0 +1,781 @@
shared_examples_for 'permission models' do
include Carto::Factories::Visualizations
before(:all) do
CartoDB::Varnish.any_instance.stubs(:send_command).returns(true)
::User.any_instance.stubs(:gravatar).returns(nil)
@user = create_user(quota_in_bytes: 524288000, table_quota: 500)
@carto_user = Carto::User.find(@user.id)
@user2 = create_user
@user3 = create_user
@user4 = create_user
@viewer_user = create_user(viewer: true)
end
after(:all) do
bypass_named_maps
@viewer_user.destroy
@user4.destroy
@user3.destroy
@user2.destroy
@user.destroy
end
describe '#create' do
it 'tests basic creation' do
visualization = FactoryGirl.create(:carto_visualization, user: Carto::User.find(@user.id))
entity_id = visualization.id
entity_type = Permission::ENTITY_TYPE_VISUALIZATION
permission = permission_from_visualization_id(entity_id)
permission.id.should_not eq nil
permission.acl.should eq []
permission.owner_id.should eq @user.id
permission.owner_username.should eq @user.username
permission.entity_id.should eq entity_id
permission.entity_type.should eq entity_type
acl_with_data = [
{
type: Permission::TYPE_USER,
entity: {
id: @user.id,
username: @user.username,
avatar_url: @user.avatar_url
},
access: Permission::ACCESS_READONLY
}
]
acl_with_data_expected = [
{
type: acl_with_data[0][:type],
id: acl_with_data[0][:entity][:id],
access: acl_with_data[0][:access]
}
]
permission.acl = acl_with_data
permission.save
permission.acl.should eq acl_with_data_expected
permission.acl = nil
permission.acl.should eq []
# Missing owner
permission2 = permission_klass.new
expect {
permission2.save
}.to raise_exception
# Owner helper methods
permission2 = permission_klass.new
user = user_for_association_from_id(@user.id)
permission2.owner = user
permission2.save
permission2.owner.should eq user
permission2.owner_id.should eq @user.id
permission2.owner_username.should eq @user.username
permission2.delete
# Entity helper methods
visualization2 = FactoryGirl.create(:carto_visualization, user: Carto::User.find(@user.id))
permission2 = permission_from_visualization_id(visualization2.id)
# invalid ACL formats
expect {
permission2.acl = 'not an array!'
}.to raise_exception CartoDB::PermissionError
expect {
permission2.acl = [
'aaa'
]
}.to raise_exception CartoDB::PermissionError
expect {
permission2.acl = [
{}
]
}.to raise_exception CartoDB::PermissionError
expect {
permission2.acl = [
{
# missing fields
wadus: 'aaa'
}
]
}.to raise_exception CartoDB::PermissionError
permission2.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_READONLY,
# Extra undesired field
wadus: 'aaa'
}
]
# Wrong permission access
expect {
permission2.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: '123'
}
]
}.to raise_exception CartoDB::PermissionError
visualization.destroy
visualization2.destroy
end
it 'deals with deleted users gracefully' do
user_to_be_deleted = create_user
map, table, table_visualization, visualization = create_full_visualization(
@carto_user,
visualization_attributes: { type: Carto::Visualization::TYPE_CANONICAL }
)
entity_id = table_visualization.id
permission = permission_from_visualization_id(entity_id)
acl_with_data = [
{
type: Permission::TYPE_USER,
entity: {
id: @user.id,
username: @user.username,
avatar_url: @user.avatar_url
},
access: Permission::ACCESS_READONLY
},
{
type: Permission::TYPE_USER,
entity: {
id: user_to_be_deleted.id,
username: user_to_be_deleted.username,
avatar_url: user_to_be_deleted.avatar_url
},
access: Permission::ACCESS_READWRITE
}
]
permission.acl = acl_with_data
permission.save
# here there's a possible race condition, when updating permissions
user_to_be_deleted.delete
acl_from_db = PermissionPresenter.new(permission).to_poro[:acl]
filtered_acl = acl_from_db.map do |entry|
{
type: entry[:type],
entity: entry[:entity].select { |k, _v| Carto::Permission::ALLOWED_ENTITY_KEYS.include?(k) },
access: entry[:access]
}
end
permission.acl = filtered_acl
permission.save
destroy_full_visualization(map, table, table_visualization, visualization)
end
it 'fails granting write permission for viewer users' do
visualization = FactoryGirl.create(:carto_visualization, type: 'table', user: Carto::User.find(@user.id))
permission = permission_from_visualization_id(visualization.id)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @viewer_user.id,
username: @viewer_user.username
},
access: Permission::ACCESS_READWRITE
}
]
expect {
save_permission(permission)
}.to raise_error(validation_error_klass, /grants write to viewers: #{@viewer_user.username}/)
visualization.destroy
end
it 'allows granting read permission for viewer users' do
map, table, table_visualization, visualization = create_full_visualization(
@carto_user,
visualization_attributes: { type: Carto::Visualization::TYPE_CANONICAL }
)
permission = permission_from_visualization_id(table_visualization.id)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @viewer_user.id,
username: @viewer_user.username
},
access: Permission::ACCESS_READONLY
}
]
permission.save
permission.reload
permission.permission_for_user(@viewer_user).should eq Permission::ACCESS_READONLY
destroy_full_visualization(map, table, table_visualization, visualization)
end
it 'changes RW to RO permission to builders becoming viewers' do
changing_user = create_user(viewer: false)
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
permission = permission_from_visualization_id(table_visualization.id)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: changing_user.id,
username: changing_user.username
},
access: Permission::ACCESS_READWRITE
}
]
Table.any_instance.expects(:add_read_write_permission).once.returns(true)
permission.save
permission.reload
permission.permission_for_user(changing_user).should eq CartoDB::Permission::ACCESS_READWRITE
changing_user.viewer = true
Table.any_instance.expects(:remove_access).once.returns(true)
Table.any_instance.expects(:add_read_permission).once.returns(true)
changing_user.save
changing_user.reload
permission.reload
permission.permission_for_user(changing_user).should eq CartoDB::Permission::ACCESS_READONLY
destroy_full_visualization(map, table, table_visualization, visualization)
changing_user.destroy
end
it 'supports groups' do
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
entity_id = visualization.id
entity_type = Permission::ENTITY_TYPE_VISUALIZATION
organization = Carto::Organization.find(FactoryGirl.create(:organization).id)
group = FactoryGirl.create(:random_group, organization: organization)
acl_initial = []
acl_with_data = [
{
type: Permission::TYPE_GROUP,
entity: {
id: group.id,
username: 'a_group'
},
access: Permission::ACCESS_READONLY
}
]
acl_with_data_expected = [
{
type: acl_with_data[0][:type],
id: acl_with_data[0][:entity][:id],
access: acl_with_data[0][:access]
}
]
permission = permission_from_visualization_id(visualization.id)
permission.id.should_not eq nil
permission.acl.should eq acl_initial
permission.owner_id.should eq @user.id
permission.owner_username.should eq @user.username
permission.entity_id.should eq entity_id
permission.entity_type.should eq entity_type
permission.acl = acl_with_data
permission.save
permission.acl.should eq acl_with_data_expected
permission.acl = nil
permission.acl.should eq acl_initial
destroy_full_visualization(map, table, table_visualization, visualization)
end
end
describe '#permissions_methods' do
it 'checks permission_for_user and is_owner methods' do
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
permission = permission_from_visualization_id(table_visualization.id)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_READONLY
}
]
permission.save
permission = permission_from_visualization_id(table_visualization.id)
permission.should_not eq nil
permission.is_owner?(@user).should eq true
permission.is_owner?(@user2).should eq false
permission.is_owner?(@user3).should eq false
permission.permission_for_user(@user).should eq Permission::ACCESS_READWRITE
permission.permission_for_user(@user2).should eq Permission::ACCESS_READONLY
permission.permission_for_user(@user3).should eq Permission::ACCESS_NONE
destroy_full_visualization(map, table, table_visualization, visualization)
end
it 'checks is_permitted' do
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
permission = permission_from_visualization_id(table_visualization.id)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_READONLY
},
{
type: Permission::TYPE_USER,
entity: {
id: @user3.id,
username: @user3.username
},
access: Permission::ACCESS_READWRITE
}
]
permission.save
permission.permitted?(@user2, Permission::ACCESS_READONLY).should eq true
permission.permitted?(@user2, Permission::ACCESS_READWRITE).should eq false
permission.permitted?(@user3, Permission::ACCESS_READONLY).should eq true
permission.permitted?(@user3, Permission::ACCESS_READWRITE).should eq true
permission.permitted?(@user4, Permission::ACCESS_READONLY).should eq false
permission.permitted?(@user4, Permission::ACCESS_READWRITE).should eq false
destroy_full_visualization(map, table, table_visualization, visualization)
end
it 'checks organizations vs users permissions precedence' do
organization = create_organization_with_owner
org_user = create_user(organization: organization, account_type: 'ORGANIZATION USER')
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
permission = permission_from_visualization_id(table_visualization.id)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: org_user.id,
username: org_user.username
},
access: Permission::ACCESS_READWRITE
},
{
type: Permission::TYPE_ORGANIZATION,
entity: {
id: organization.id,
username: organization.name
},
access: Permission::ACCESS_READONLY
}
]
permission.save
permission.permitted?(org_user, Permission::ACCESS_READONLY).should eq true
permission.permitted?(org_user, Permission::ACCESS_READWRITE).should eq true
# Organization has more permissions than user. Should get inherited (RW prevails)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: org_user.id,
username: org_user.username
},
access: Permission::ACCESS_READONLY
},
{
type: Permission::TYPE_ORGANIZATION,
entity: {
id: organization.id,
username: organization.name
},
access: Permission::ACCESS_READWRITE
}
]
permission.save
permission.permitted?(org_user, Permission::ACCESS_READONLY).should eq true
permission.permitted?(org_user, Permission::ACCESS_READWRITE).should eq true
# Organization permission only
permission.acl = [
{
type: Permission::TYPE_ORGANIZATION,
entity: {
id: organization.id,
username: organization.name
},
access: Permission::ACCESS_READWRITE
}
]
permission.save
permission.permitted?(org_user, Permission::ACCESS_READONLY).should eq true
permission.permitted?(org_user, Permission::ACCESS_READWRITE).should eq true
destroy_full_visualization(map, table, table_visualization, visualization)
organization.destroy_cascade
end
end
describe '#shared_entities' do
before(:each) do
@map, @table, @table_visualization, @visualization = create_full_visualization(@carto_user)
end
after(:each) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
end
it 'tests the management of shared entities upon permission save (based on its ACL)' do
entity_id = @table_visualization.id
permission = permission_from_visualization_id(entity_id)
# Create old entries
CartoDB::SharedEntity.new(
recipient_id: @user.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: entity_id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
).save
CartoDB::SharedEntity.new(
recipient_id: @user2.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: entity_id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
).save
CartoDB::SharedEntity.where(entity_id: entity_id).count.should eq 2
# Leave only user 1
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user.id,
username: @user.username
},
access: Permission::ACCESS_READONLY
}
]
# shared entry of user 2 should dissapear
permission.save
CartoDB::SharedEntity.where(entity_id: entity_id).count.should eq 1
shared_entity = CartoDB::SharedEntity.where(entity_id: entity_id).first
shared_entity.recipient_id.should eq @user.id
shared_entity.entity_id.should eq entity_id
# Leave only user 2 now
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_READWRITE
}
]
permission.save
CartoDB::SharedEntity.where(entity_id: entity_id).count.should eq 1
shared_entity = CartoDB::SharedEntity.where(entity_id: entity_id).first
shared_entity.recipient_id.should eq @user2.id
shared_entity.entity_id.should eq entity_id
# Now just change permission, user2 should still be there
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_READONLY
}
]
permission.save
CartoDB::SharedEntity.where(entity_id: entity_id).count.should eq 1
shared_entity = CartoDB::SharedEntity.where(entity_id: entity_id).first
shared_entity.recipient_id.should eq @user2.id
shared_entity.entity_id.should eq entity_id
# Now set a permission forbidding a user, so should dissapear too
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_NONE
}
]
permission.save
CartoDB::SharedEntity.where(entity_id: entity_id).count.should eq 0
end
it 'triggers table metadata update (to purgue caches, for example) if a permission is removed' do
entity_id = @table_visualization.id
permission = permission_from_visualization_id(entity_id)
CartoDB::SharedEntity.new(
recipient_id: @user.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: entity_id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
).save
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user.id,
username: @user.username
},
access: Permission::ACCESS_NONE
}
]
# Twice: once at model `update_changes` and one on permission destroy (at cleanup)
::Table.any_instance.expects(:update_cdb_tablemetadata).twice
permission.save
end
end
describe "permissions diffs" do
it "should enqueue the right notification email" do
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
entity_id = visualization.id
permission = permission_from_visualization_id(entity_id)
user2_id = "17d5b1e6-0d14-11e4-a3ef-0800274a1928"
user3_id = "28d09bc0-0d14-11e4-a3ef-0800274a1928"
permissions_changes = {
"user" => {
user2_id => [{ "action" => "grant", "type" => "r" }],
user3_id => [{ "action" => "revoke", "type" => "r" }]
}
}
::Resque.stubs(:enqueue).returns(nil)
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::ShareVisualization, permission.entity.id, user2_id).once
::Resque.expects(:enqueue).with(::Resque::UserJobs::Mail::UnshareVisualization,
permission.entity.name, permission.owner_username, user3_id).once
permission.notify_permissions_change(permissions_changes)
destroy_full_visualization(map, table, table_visualization, visualization)
end
it "should call the permissions comparison function with correct values" do
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
entity_id = visualization.id
permission = permission_from_visualization_id(entity_id)
Resque.stubs(:enqueue).returns(nil)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_READONLY
}
]
acl1 = permission.acl
permission.save
# Here try full reload of model
permission = permission_from_visualization_id(entity_id)
permission.acl = [
{
type: Permission::TYPE_USER,
entity: {
id: @user2.id,
username: @user2.username
},
access: Permission::ACCESS_READONLY
},
{
type: Permission::TYPE_USER,
entity: {
id: @user3.id,
username: @user3.username
},
access: Permission::ACCESS_READONLY
}
]
acl2 = permission.acl
CartoDB::Permission.expects(:compare_new_acl).with(acl1, acl2).once
permission.save
CartoDB::Permission.expects(:compare_new_acl).with(acl2, []).once
# Hack to workaround FK while keeping the destroy test
fake_permission = FactoryGirl.create(:carto_permission, owner_id: @user.id)
visualization.update_column(:permission_id, fake_permission.id)
permission.destroy
CartoDB::Permission.expects(:compare_new_acl).with([], []).at_least_once
destroy_full_visualization(map, table, table_visualization, visualization)
end
it "permission comparisson function should return right diff hash" do
acl1 = [
{ type: "user", id: "17d09bc0-0d14-11e4-a3ef-0800274a1928", access: "r" },
{ type: "user", id: "28d09bc0-0d14-11e4-a3ef-0800274a1928", access: "r" }
]
acl2 = [
{ type: "user", id: "17d09bc0-0d14-11e4-a3ef-0800274a1928", access: "r" },
{ type: "user", id: "17d5b1e6-0d14-11e4-a3ef-0800274a1928", access: "r" }
]
expected_diff = {
"user" => {
"17d5b1e6-0d14-11e4-a3ef-0800274a1928" => [{ "action" => "grant", "type" => "r" }],
"28d09bc0-0d14-11e4-a3ef-0800274a1928" => [{ "action" => "revoke", "type" => "r" }]
}
}
CartoDB::Permission.compare_new_acl(acl1, acl2).should == expected_diff
end
end
describe '#set_user_permission and #remove_user_permission' do
it 'sets and removes user permission' do
map, table, table_visualization, visualization = create_full_visualization(@carto_user)
permission = permission_from_visualization_id(table_visualization.id)
permission.set_user_permission(@user2, Permission::ACCESS_READONLY)
permission.save
visualization = visualization_from_id(table_visualization.id)
visualization.has_permission?(@user2, Permission::ACCESS_READONLY).should be_true
visualization.has_permission?(@user2, Permission::ACCESS_READWRITE).should be_false
# Permission is reassigned because current design doesn't works well with several changes in a row.
permission = permission_from_visualization_id(table_visualization.id)
permission.remove_user_permission(@user2)
permission.save
visualization = visualization_from_id(table_visualization.id)
visualization.has_permission?(@user2, Permission::ACCESS_READONLY).should be_false
visualization.has_permission?(@user2, Permission::ACCESS_READWRITE).should be_false
permission = permission_from_visualization_id(table_visualization.id)
permission.set_user_permission(@user2, Permission::ACCESS_READWRITE)
permission.save
visualization = visualization_from_id(table_visualization.id)
visualization.has_permission?(@user2, Permission::ACCESS_READONLY).should be_true
visualization.has_permission?(@user2, Permission::ACCESS_READWRITE).should be_true
destroy_full_visualization(map, table, table_visualization, visualization)
end
end
describe 'permission cleanup on deletions' do
describe 'sharing' do
include_context 'organization with users helper'
include Carto::Factories::Visualizations
before(:all) do
@group = FactoryGirl.create(:random_group, organization_id: @organization.id)
end
after(:all) do
@group.destroy
end
it 'deletes ACL entries from permissions on user deletion' do
map, table, table_visualization, visualization = create_full_visualization(@carto_org_user_1)
permission = table.permission
permission.acl = [
{
type: Permission::TYPE_USER,
entity: { id: @carto_org_user_2.id },
access: Permission::ACCESS_READONLY
}
]
permission.save
permission.acl.map { |a| a[:id] }.should include(@carto_org_user_2.id)
permission.entities_with_read_permission.should include(@carto_org_user_2)
@org_user_2.destroy
permission.reload
permission.acl.map { |a| a[:id] }.should_not include(@carto_org_user_2.id)
permission.entities_with_read_permission.should_not include(@carto_org_user_2)
destroy_full_visualization(map, table, table_visualization, visualization)
end
it 'deletes ACL entries from permissions on deletion' do
map, table, table_visualization, visualization = create_full_visualization(@carto_org_user_1)
Carto::Group.any_instance.expects(:grant_db_permission).once
permission = table.permission
permission.acl = [
{
type: Permission::TYPE_GROUP,
entity: { id: @group.id },
access: Permission::ACCESS_READONLY
}
]
permission.save
permission.acl.map { |a| a[:id] }.should include(@group.id)
permission.entities_with_read_permission.should include(@group)
@group.destroy
permission.reload
permission.acl.map { |a| a[:id] }.should_not include(@group.id)
permission.entities_with_read_permission.should_not include(@group)
destroy_full_visualization(map, table, table_visualization, visualization)
end
end
end
end
@@ -0,0 +1,87 @@
require_relative '../../spec_helper'
include CartoDB
describe CartoDB::PlatformLimits::Importer::UserConcurrentImportsAmount do
before(:all) do
@user = create_user( :quota_in_bytes => 524288000, :table_quota => 100 )
end
before(:each) do
end
after(:all) do
bypass_named_maps
@user.destroy
end
describe '#importer-limits' do
it 'checks concurrent imports rate limit' do
old_concurrent_import_count = @user.max_concurrent_import_count
new_limit_max = 2
@user.update max_concurrent_import_count: new_limit_max
# Set TTL threshold to 0 to ease calculations
limit = CartoDB::PlatformLimits::Importer::UserConcurrentImportsAmount.new({
user: @user,
redis: {
db: $users_metadata,
expire_ttl_threshold_percent: 0
}
})
limit.peek.should eq 0
limit.is_over_limit?.should eq false
limit.peek.should eq 0
limit.maximum_limit?.should eq new_limit_max
limit.remaining_limit?.should eq new_limit_max
start_datetime = DateTime.now
limit.is_over_limit!.should eq false
end_datetime = limit.time_period?
limit.peek.should eq 1
limit.remaining_limit?.should eq new_limit_max-1
limit.is_over_limit?.should eq false
limit.is_over_limit!.should eq false
limit.peek.should eq 2
limit.is_over_limit!.should eq true
limit.peek.should eq 3
limit.is_over_limit?.should eq true
limit.peek.should eq 3
limit.decrement!
limit.peek.should eq 2
limit.is_over_limit?.should eq false
ttl = end_datetime - start_datetime
# We redis won't take more than 2 seconds on performing key creation + retrieval of TTL
(ttl - (@user.database_timeout/1000)).should be_within(2.0).of(0.0)
limit.decrement!
limit.decrement!
limit.peek.should eq 0
limit.is_within_limit!.should eq true
limit.peek.should eq 1
limit.is_within_limit?.should eq true
limit.peek.should eq 1
limit.is_within_limit!.should eq true
limit.peek.should eq 2
limit.is_within_limit!.should eq false
limit.peek.should eq 3
limit.is_within_limit?.should eq false
limit.peek.should eq 3
@user.update max_concurrent_import_count: old_concurrent_import_count
end
end
end
@@ -0,0 +1,70 @@
require_relative '../../spec_helper'
include CartoDB
describe CartoDB::PlatformLimits::Importer::UserConcurrentSyncsAmount do
describe '#importer-limits' do
before(:all) do
@user = FactoryGirl.create(:valid_user)
end
after(:all) do
@user.destroy
end
it 'checks concurrent sync rate limit' do
syncs_limit = CartoDB::PlatformLimits::Importer::UserConcurrentSyncsAmount::MAX_SYNCS_PER_USER
limit = CartoDB::PlatformLimits::Importer::UserConcurrentSyncsAmount.new({
user: @user,
redis: {
db: $users_metadata
}
})
limit.peek.should eq 0
limit.is_over_limit?.should eq false
limit.peek.should eq 0
limit.maximum_limit?.should eq syncs_limit
limit.remaining_limit?.should eq syncs_limit
limit.is_over_limit!.should eq false
limit.peek.should eq 1
limit.remaining_limit?.should eq syncs_limit-1
limit.is_over_limit?.should eq false
limit.is_over_limit!.should eq false
limit.peek.should eq 2
limit.is_over_limit?.should eq false
# First increments, then checks
limit.is_over_limit!.should eq true
limit.peek.should eq 3
limit.is_over_limit?.should eq true
limit.is_over_limit!.should eq true
limit.is_over_limit?.should eq true
limit.peek.should eq 3
limit.decrement!
limit.peek.should eq 2
limit.is_over_limit?.should eq false
limit.decrement!
limit.peek.should eq 1
limit.decrement!
limit.decrement!
limit.decrement!
limit.peek.should eq 0
limit.is_within_limit!.should eq true
limit.peek.should eq 1
limit.is_within_limit!.should eq true
limit.peek.should eq 2
limit.is_within_limit!.should eq false
limit.peek.should eq 3
end
end
end
@@ -0,0 +1,81 @@
require_relative '../spec_helper'
shared_examples_for 'shared entity models' do
before(:all) do
CartoDB::Varnish.any_instance.stubs(:send_command).returns(true)
@user = create_user(:quota_in_bytes => 524288000, :table_quota => 500)
end
after(:all) do
bypass_named_maps
@user.destroy
end
describe '#create' do
it 'tests basic creation and validation' do
recipient_id = UUIDTools::UUID.timestamp_create.to_s
entity_id = UUIDTools::UUID.timestamp_create.to_s
shared_entity_class.where(entity_id: entity_id).count.should eq 0
shared_entity = shared_entity_class.new(
recipient_id: recipient_id,
recipient_type: shared_entity_class::RECIPIENT_TYPE_USER,
entity_id: entity_id,
entity_type: shared_entity_class::ENTITY_TYPE_VISUALIZATION
)
shared_entity.valid?.should eq true
shared_entity.errors.should be_empty
shared_entity.save
shared_entity_class.where(entity_id: entity_id).count.should eq 1
shared_entity2 = shared_entity_class.new(
recipient_id: recipient_id,
recipient_type: shared_entity_class::RECIPIENT_TYPE_USER,
entity_id: entity_id,
entity_type: shared_entity_class::ENTITY_TYPE_VISUALIZATION
)
shared_entity2.valid?.should eq false
# Workaround for cross-ORM compatibility
# shared_entity2.errors.should eq({[:recipient_id, :entity_id]=>['is already taken']})
shared_entity2.errors.values.should eq([["is already taken"]])
shared_entity.destroy
shared_entity_class.where(entity_id: entity_id).count.should eq 0
shared_entity = shared_entity_class.new(
recipient_id: entity_id,
recipient_type: shared_entity_class::RECIPIENT_TYPE_USER,
entity_type: shared_entity_class::ENTITY_TYPE_VISUALIZATION
)
shared_entity.valid?.should eq false
shared_entity = shared_entity_class.new(
recipient_id: recipient_id,
recipient_type: shared_entity_class::RECIPIENT_TYPE_USER,
entity_type: shared_entity_class::ENTITY_TYPE_VISUALIZATION
)
shared_entity.valid?.should eq false
shared_entity = shared_entity_class.new(
recipient_id: recipient_id,
recipient_type: shared_entity_class::RECIPIENT_TYPE_USER,
entity_id: entity_id,
)
shared_entity.valid?.should eq false
shared_entity = shared_entity_class.new(
)
shared_entity.valid?.should eq false
shared_entity = shared_entity_class.new(
recipient_id: recipient_id,
recipient_type: shared_entity_class::RECIPIENT_TYPE_USER,
entity_id: entity_id,
entity_type: 'whatever'
)
shared_entity.valid?.should eq false
end
end
end
+9
View File
@@ -0,0 +1,9 @@
require_relative 'shared_entity_shared_examples'
describe CartoDB::SharedEntity do
it_behaves_like 'shared entity models' do
def shared_entity_class
CartoDB::SharedEntity
end
end
end
@@ -0,0 +1,61 @@
require_relative '../../spec_helper'
require_relative '../../../services/data-repository/backend/sequel'
require_relative '../../../services/data-repository/repository'
require_relative '../../../app/models/synchronization/collection'
require_relative '../../../app/models/synchronization/member'
require 'helpers/unique_names_helper'
include UniqueNamesHelper
include CartoDB
describe Synchronization::Collection do
before(:each) do
end
describe '#fetch' do
it 'fetches the members of a collection' do
user_id = random_uuid
Synchronization::Member.new(random_attributes(name: 'sync_1',
user_id: user_id)).store
Synchronization::Member.new(random_attributes(name: 'sync_2',
user_id: user_id)).store
collection = Synchronization::Collection.new
records = collection.fetch(user_id: user_id)
records.first.name.should == 'sync_1'
records.to_a.last.name.should == 'sync_2'
end
end
describe '#fetch_many' do
it 'fetches many members of the collection to see that paging works' do
user_id = random_uuid
(1..400).each do |idx|
Synchronization::Member.new(random_attributes(name: "sync_#{idx}",
user_id: user_id)).store
end
collection = Synchronization::Collection.new
records = collection.fetch(user_id: user_id)
records.first.name.should == 'sync_1'
records.to_a.last.name.should == 'sync_300'
records.count.should == 300
records = collection.fetch(user_id: user_id, per_page: 900)
records.first.name.should == 'sync_1'
records.to_a.last.name.should == 'sync_400'
records.count.should == 400
end
end
def random_attributes(attributes = {})
random = unique_integer
{
name: attributes.fetch(:name, "name #{random}"),
interval: attributes.fetch(:interval, 900),
state: attributes.fetch(:state, 'enabled'),
user_id: attributes.fetch(:user_id, random_uuid)
}
end
end # Synchronization::Collection
@@ -0,0 +1,176 @@
require_relative '../../spec_helper'
require_relative '../../../services/data-repository/backend/sequel'
require_relative '../../../services/data-repository/repository'
require_relative '../../../app/models/synchronization/member'
require 'helpers/unique_names_helper'
require 'helpers/file_server_helper'
require_relative '../../helpers/feature_flag_helper'
include UniqueNamesHelper
include CartoDB
describe Synchronization::Member do
describe "synchronizations" do
before(:all) do
@user1 = create_user(sync_tables_enabled: true)
@user2 = create_user(sync_tables_enabled: true)
@feature_flag = FactoryGirl.create(:feature_flag, name: 'create_overviews', restricted: true)
end
before(:each) do
bypass_named_maps
end
around(:each) do |example|
Cartodb.with_config(metrics: {}, &example)
end
after(:all) do
@user1.destroy
@user2.destroy
end
include FeatureFlagHelper
def overview_tables(user, table)
overviews = user.in_database do |db|
db.fetch %{
SELECT * FROM CDB_Overviews('#{table}'::regclass)
}
end
overviews.map(:overview_table)
end
def has_overviews?(user, table)
!overview_tables(user, table).empty?
end
def remove_overviews(user, table)
user.in_database do |db|
db.run %{
SELECT CDB_DropOverviews('#{table}'::regclass)
}
end
end
describe "synchronization" do
it 'creates overviews' do
CartoDB::Logger.stubs(:error).never
url = 'https://wadus.com/cities-box.csv'
path = "#{Rails.root}/spec/support/data/cities-box.csv"
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user1.id).merge(service_item_id: url, url: url, name: 'cities_box')
member = Synchronization::Member.new(attrs).store
set_feature_flag @user1, 'create_overviews', true
# Import data without overviews needed (too few rows for overviews)
data_import = Cartodb.with_config overviews: { 'min_rows' => 1000 } do
DataImport.create(
user_id: @user1.id,
data_source: path,
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now,
privacy: ::UserTable::PRIVACY_VALUES_TO_TEXTS.invert['public']
).run_import!
end
table_name = UserTable[id: data_import.table.id].name
has_overviews?(@user1, table_name).should eq false
# Now synchronize with overviews needed
Cartodb.with_config overviews: { 'min_rows' => 100 } do
member.run
end
has_overviews?(@user1, table_name).should eq true
end
it 'deletes overviews' do
CartoDB::Logger.stubs(:error).never
url = 'https://wadus.com/cities-box.csv'
path = "#{Rails.root}/spec/support/data/cities-box.csv"
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user1.id).merge(service_item_id: url, url: url, name: 'cities_box')
member = Synchronization::Member.new(attrs).store
set_feature_flag @user1, 'create_overviews', true
# Import data with overviews needed (too few rows for overviews)
data_import = Cartodb.with_config overviews: { 'min_rows' => 100 } do
DataImport.create(
user_id: @user1.id,
data_source: path,
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now,
privacy: ::UserTable::PRIVACY_VALUES_TO_TEXTS.invert['public']
).run_import!
end
table_name = UserTable[id: data_import.table.id].name
member.name = table_name
has_overviews?(@user1, table_name).should eq true
# Now synchronize without overviews needed
Cartodb.with_config overviews: { 'min_rows' => 1000 } do
member.run
end
has_overviews?(@user1, table_name).should eq false
end
it 'updates overviews' do
CartoDB::Logger.stubs(:error).never
url = 'https://wadus.com/cities-box.csv'
path = "#{Rails.root}/spec/support/data/cities-box.csv"
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user1.id).merge(service_item_id: url, url: url, name: 'cities_box')
member = Synchronization::Member.new(attrs).store
set_feature_flag @user1, 'create_overviews', true
# Import data with overviews needed (too few rows for overviews)
data_import = Cartodb.with_config overviews: { 'min_rows' => 100 } do
DataImport.create(
user_id: @user1.id,
data_source: path,
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now,
privacy: ::UserTable::PRIVACY_VALUES_TO_TEXTS.invert['public']
).run_import!
end
table_name = UserTable[id: data_import.table.id].name
has_overviews?(@user1, table_name).should eq true
# Now synchronize with overviews needed
# TODO: alter or delete overviews contents and check they're are re-generated
Cartodb.with_config overviews: { 'min_rows' => 100 } do
member.run
end
has_overviews?(@user1, table_name).should eq true
end
end
end
private
def random_attributes(attributes = {})
random = unique_integer
{
name: attributes.fetch(:name, "name#{random}"),
interval: attributes.fetch(:interval, 15 * 60 + random),
state: attributes.fetch(:state, 'enabled'),
user_id: attributes.fetch(:user_id, nil)
}
end
end
+277
View File
@@ -0,0 +1,277 @@
require_relative '../../spec_helper'
require_relative '../../../services/data-repository/backend/sequel'
require_relative '../../../services/data-repository/repository'
require_relative '../../../app/models/synchronization/member'
require 'helpers/unique_names_helper'
require 'helpers/file_server_helper'
include UniqueNamesHelper
include CartoDB
describe Synchronization::Member do
describe 'Basic actions' do
it 'assigns an id by default' do
member = Synchronization::Member.new
member.should be_an_instance_of Synchronization::Member
member.id.should_not be_nil
end
it 'persists attributes to the repository' do
attributes = random_attributes
member = Synchronization::Member.new(attributes)
member.store
member = Synchronization::Member.new(id: member.id)
member.name.should be_nil
member.fetch
member.name .should == attributes.fetch(:name)
end
it 'fetches attributes from the repository' do
attributes = random_attributes
member = Synchronization::Member.new(attributes).store
member = Synchronization::Member.new(id: member.id)
member.name = 'changed'
member.fetch
member.name.should == attributes.fetch(:name)
end
it 'deletes this member from the repository' do
member = Synchronization::Member.new(random_attributes).store
member.fetch
member.name.should_not be_nil
member.delete
member.name.should be_nil
lambda { member.fetch }.should raise_error KeyError
end
end
describe "synchronizations" do
before(:all) do
@user1 = create_user(sync_tables_enabled: true)
@user2 = create_user(sync_tables_enabled: true)
end
before(:each) do
bypass_named_maps
end
around(:each) do |example|
Cartodb.with_config(metrics: {}, &example)
end
after(:all) do
@user1.destroy
@user2.destroy
end
describe 'external sources' do
it "Authorizes to sync always if from an external source" do
member = Synchronization::Member.new(random_attributes(user_id: @user1.id)).store
member.fetch
member.expects(:from_external_source?).returns(true)
@user1.sync_tables_enabled = true
@user2.sync_tables_enabled = true
member.authorize?(@user1).should eq true
member.authorize?(@user2).should eq false
@user1.sync_tables_enabled = false
@user2.sync_tables_enabled = false
member.authorize?(@user1).should eq true
end
end
describe "synchronization" do
it 'syncs' do
# TODO: this is the minimum test valid to reproduce #11889, it's not a complete sync test
CartoDB::Logger.stubs(:error).never
url = 'https://wadus.com/guess_country.csv'
path = fake_data_path('guess_country.csv')
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user1.id).merge(service_item_id: url, url: url, name: 'guess_country')
member = Synchronization::Member.new(attrs).store
DataImport.create(
user_id: @user1.id,
data_source: fake_data_path('guess_country.csv'),
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now
).run_import!
member.run
end
it 'fails if user is inactive' do
url = 'https://wadus.com/guess_country.csv'
path = fake_data_path('guess_country.csv')
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user1.id).merge(service_item_id: url, url: url, name: 'guess_country')
member = Synchronization::Member.new(attrs).store
DataImport.create(
user_id: @user1.id,
data_source: fake_data_path('guess_country.csv'),
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now
).run_import!
@user1.state = Carto::User::STATE_LOCKED
@user1.save
CartoDB::Logger.stubs(:error).once
member.fetch.run
member.log.entries.should match /Can't run a synchronization for inactive user/
expect(member.state).to eq 'failure'
@user1.state = Carto::User::STATE_ACTIVE
@user1.sync_tables_enabled = true
@user1.save
@user1.reload
end
it 'fails to overwrite tables with views by replacement' do
url = 'https://wadus.com/guess_country.csv'
path = fake_data_path('guess_country.csv')
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user2.id).merge(service_item_id: url, url: url, name: 'guess_country')
member = Synchronization::Member.new(attrs).store
DataImport.create(
user_id: @user2.id,
data_source: path,
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now
).run_import!
@user2.in_database.execute('CREATE VIEW wadus AS SELECT * FROM guess_country')
member.run
expect(member.state).to eq 'failure'
expect(member.error_code).to eq 2013
end
it 'it can overwrite tables with views by sync' do
url = 'https://wadus.com/guess_country_geocoded.csv'
path = fake_data_path('guess_country_geocoded.csv')
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user2.id).merge(service_item_id: url, url: url, name: 'guess_country_geocoded')
member = Synchronization::Member.new(attrs).store
DataImport.create(
user_id: @user2.id,
data_source: path,
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now
).run_import!
@user2.in_database.execute('CREATE VIEW wadus_geocoded AS SELECT * FROM guess_country_geocoded')
member.run
expect(member.state).to eq 'success'
end
it 'should sync files with missing ogc_fid' do
stub_arcgis_response_with_file(
File.expand_path('spec/fixtures/arcgis_response_missing_ogc_fid.json'),
File.expand_path('spec/fixtures/arcgis_metadata_ogc_fid.json')
)
url = 'https://wtf.com/arcgis/rest/services/Planning/EPI_Primary_Planning_Layers/MapServer/2'
attrs = random_attributes(user_id: @user1.id)
.merge(service_item_id: url, url: url, name: 'land_zoning')
member = Synchronization::Member.new(attrs).store
data_import = DataImport.create(
user_id: @user1.id,
synchronization_id: member.id,
service_name: 'arcgis',
service_item_id: url,
updated_at: Time.now
)
data_import.run_import!
expect(data_import.state).to eq 'complete'
source_file = CartoDB::Importer2::SourceFile.new(
File.expand_path('spec/fixtures/arcgis_response_missing_ogc_fid.json'),
'arcgis_response_missing_ogc_fid.json'
)
CartoDB::Importer2::Downloader.any_instance.stubs(:download_and_store).returns(source_file)
CartoDB::Importer2::Downloader.any_instance.stubs(:source_file).returns(source_file)
member.run
expect(member.state).to eq 'success'
end
it 'keeps indices' do
url = 'https://wadus.com/clubbing.csv'
path = fake_data_path('clubbing.csv')
stub_download(url: url, filepath: path, content_disposition: false)
attrs = random_attributes(user_id: @user2.id).merge(service_item_id: url, url: url, name: 'clubbing')
member = Synchronization::Member.new(attrs).store
# Create table with index
DataImport.create(
user_id: @user2.id,
data_source: path,
synchronization_id: member.id,
service_name: 'public_url',
service_item_id: url,
updated_at: Time.now
).run_import!
@user2.in_database.execute('CREATE INDEX ON clubbing (nombre)')
# Sync the table
member.run
expect(member.state).to eq 'success'
# Expect custom and default indices to still exist
table = UserTable.where(user: @user2, name: 'clubbing').first
indexed_columns = table.service.pg_indexes.map { |x| x[:column] }
expected_indices = ['cartodb_id', 'the_geom', 'the_geom_webmercator', 'nombre']
expect(indexed_columns.sort).to eq(expected_indices.sort)
end
end
end
private
def random_attributes(attributes={})
random = unique_integer
{
name: attributes.fetch(:name, "name#{random}"),
interval: attributes.fetch(:interval, 15 * 60 + random),
state: attributes.fetch(:state, 'enabled'),
user_id: attributes.fetch(:user_id, nil)
}
end
end
@@ -0,0 +1,160 @@
require 'spec_helper'
describe SynchronizationOauth do
before(:all) do
@user = create_user(:quota_in_bytes => 500.megabytes, :table_quota => 500)
@user.reload
end
after(:all) do
bypass_named_maps
@user.destroy
end
after(:each) do
@user.synchronization_oauths.map &:destroy
@user.reload
end
context '#creation_updating' do
it 'Tests basic creation operations' do
service_name = 'testtest'
token_value = 'qv2345q235erfaweerfdsdfsds'
token_value_2 = 'aaaaaaaaaaaaaaaaaaaaaaaa'
SynchronizationOauth.all.size.should eq 0
oauth_entry = SynchronizationOauth.create(
user_id: @user.id,
service: service_name,
token: token_value
).reload
oauth_entry.nil?.should eq false
oauth_entry.user_id.should eq @user.id
oauth_entry.service.should eq service_name
oauth_entry.token.should eq token_value
SynchronizationOauth.all.size.should eq 1
entry_second_handle = SynchronizationOauth.where(user_id: @user.id, service: service_name).first
entry_second_handle.nil?.should eq false
entry_second_handle.user_id.should eq @user.id
entry_second_handle.service.should eq service_name
entry_second_handle.token.should eq token_value
entry_second_handle.should eq oauth_entry
oauth_entry.token = token_value_2
oauth_entry.save.reload
oauth_entry.token.should eq token_value_2
expect {
oauth_entry.service = 'another_service_name'
oauth_entry.save
}.to raise_exception Sequel::ValidationFailed
oauth_entry.service = service_name
expect {
oauth_entry.user_id = UUIDTools::UUID.timestamp_create
oauth_entry.save
}.to raise_exception Sequel::ValidationFailed
expect {
SynchronizationOauth.create(
user_id: @user.id,
service: service_name,
token: UUIDTools::UUID.timestamp_create
)
}.to raise_exception Sequel::ValidationFailed
expect {
SynchronizationOauth.create(
user_id: @user_id,
service: service_name,
token: nil
)
}.to raise_exception Sequel::ValidationFailed
expect {
SynchronizationOauth.create(
user_id: @user_id,
service: service_name,
token: ''
)
}.to raise_exception Sequel::ValidationFailed
end
end
context '#deletion' do
it 'tests deletion of items' do
another_uuid = UUIDTools::UUID.timestamp_create.to_s
service_name = 'testtest'
service_name_2 = '123456'
token_value = 'qv2345q235erfaweerfdsdfsds'
token_value_2 = 'aaaaaaaaaaaaaaaaaaaaaaaa'
SynchronizationOauth.create(
user_id: @user.id,
service: service_name,
token: token_value
)
SynchronizationOauth.create(
user_id: @user.id,
service: service_name_2,
token: token_value_2
)
SynchronizationOauth.create(
user_id: another_uuid,
service: service_name_2,
token: token_value_2
)
@user.synchronization_oauths.size.should eq 2
oauth = SynchronizationOauth.where(user_id: @user.id, service: service_name).first
oauth.destroy
@user.reload
@user.synchronization_oauths.size.should eq 1
oauth = SynchronizationOauth.where(user_id: @user.id, service: service_name_2).first
oauth.destroy
@user.reload
@user.synchronization_oauths.size.should eq 0
end
end
context '#user_oauths' do
it 'tests the one to many association of user and sync oauths' do
service_name = 'testtest'
token_value = 'qv2345q235erfaweerfdsdfsds'
@user.synchronization_oauths.size.should eq 0
SynchronizationOauth.create(
user_id: @user.id,
service: service_name,
token: token_value
)
@user.reload
@user.synchronization_oauths.size.should eq 1
retrieved_entry = @user.synchronization_oauths.first
@user.synchronization_oauths.first.should eq retrieved_entry
@user.synchronization_oauths.first.destroy
@user.reload
@user.synchronization_oauths.size.should eq 0
@user.add_synchronization_oauth(service: service_name, token: token_value)
@user.synchronization_oauths.size.should eq 1
expect {
@user.add_synchronization_oauth(service: service_name, token: token_value)
}.to raise_exception Sequel::ValidationFailed
end
end
end
@@ -0,0 +1,48 @@
require_relative '../../simplecov_helper'
require_relative '../../rspec_configuration'
require_relative '../../../app/models/table/column_typecaster'
describe CartoDB::ColumnTypecaster do
describe '#pg_replace_expression' do
before(:all) do
@column_typecaster = CartoDB::ColumnTypecaster.new({
user_database: nil,
schema: nil,
table_name: nil,
column_name: nil,
new_type: nil
})
end
it 'takes a column_name and a set of replacements and returns a postgres expression that implements them' do
# e.g.: as it if where in spanish locale
thousand_separator = '.'
decimal_separator = ','
# order matters, the innermost expression will be applied first
replacements = [
[thousand_separator, ''],
[decimal_separator, '.'],
['$', '']
]
@column_typecaster.send(:pg_replace_expression, 'my_column', replacements)
.should == %Q{replace(replace(replace("my_column", '.', ''), ',', '.'), '$', '')}
end
it 'returns an expression quasi-equivalent to the previous implementation' do
# us locale
thousand_separator = ','
decimal_separator = '.'
replacements = [
[thousand_separator, ''],
[decimal_separator, '.']
]
@column_typecaster.send(:pg_replace_expression, 'my_column', replacements)
.should == %Q{replace(replace("my_column", ',', ''), '.', '.')}
end
end
end
+68
View File
@@ -0,0 +1,68 @@
require_relative '../spec_helper'
describe CartoDB::TablePrivacyManager do
before(:all) do
@user = FactoryGirl.create(:valid_user, quota_in_bytes: 524288000, table_quota: 500, private_tables_enabled: true)
end
before(:each) do
bypass_named_maps
end
after(:all) do
@user.destroy
end
it 'should not change table privacy if setting it fails' do
table = ::Table.new
table.user_id = @user.id
table.name = 'manolo'
table.privacy = Carto::UserTable::PRIVACY_PRIVATE
table.save
table.privacy == Carto::UserTable::PRIVACY_PUBLIC
table.stubs(:set_from_table_privacy) { raise StandardError }
expect { table.save to_raise }.to raise_exception StandardError
table.privacy.should == Carto::UserTable::PRIVACY_PRIVATE
end
shared_examples_for 'table create_canonical_visualization support' do
describe '#apply_privacy_change' do
it 'changes canonical visualization privacy' do
table = create_table(user_id: @user.id, name: "aaa", privacy: UserTable::PRIVACY_PRIVATE)
previous_privacy = table.instance_eval { previous_privacy }
user_table = table.instance_eval { @user_table }
privacy_changed = table.instance_eval { privacy_changed? }
manager = CartoDB::TablePrivacyManager.new(user_table)
manager.apply_privacy_change(table, previous_privacy, privacy_changed)
user_table.reload
user_table.table_visualization.privacy.should eq Carto::Visualization::PRIVACY_PRIVATE
table.privacy = UserTable::PRIVACY_PUBLIC
table.save
user_table.reload
user_table.table_visualization.privacy.should eq Carto::Visualization::PRIVACY_PUBLIC
end
end
end
describe 'legacy models' do
it_behaves_like 'table create_canonical_visualization support'
before(:each) do
Table.any_instance.stubs(:model_class).returns(::UserTable)
end
end
describe 'new models' do
it_behaves_like 'table create_canonical_visualization support'
before(:each) do
Table.any_instance.stubs(:model_class).returns(Carto::UserTable)
end
end
end
+85
View File
@@ -0,0 +1,85 @@
require 'spec_helper'
describe CartoDB::TableRegistrar do
before(:all) do
@user = create_user
end
after(:all) do
@user.destroy
end
describe '#register' do
let(:description) { "Table registrar test description" }
let(:tags) { ["Administrative regions", "Historic"] }
let(:source) { "Table registrar test source" }
let(:attributions) { "Table registrar test attributions" }
before(:each) do
bypass_named_maps
@data_import = FactoryGirl.create(:data_import, user_id: @user.id)
@external_data_import = FactoryGirl.create(:external_data_import_with_external_source,
data_import_id: @data_import.id)
@external_data_import.external_source.visualization.description = description
@external_data_import.external_source.visualization.tags = tags
@external_data_import.external_source.visualization.source = source
@external_data_import.external_source.visualization.attributions = attributions
@external_data_import.external_source.visualization.save
end
after(:each) do
@external_data_import.destroy
@data_import.destroy
end
it 'registers description from external sources' do
CartoDB::Visualization::Member.any_instance.stubs(:save_named_map).returns(true)
@user.in_database.run(%{CREATE TABLE xxx (id serial)})
tr = CartoDB::TableRegistrar.new(@user, ::Table)
tr.register('xxx', @external_data_import.data_import_id)
tr.table.description.should eq description
tr.table.table_visualization.delete
tr.table.destroy
end
it 'registers tags from external sources' do
CartoDB::Visualization::Member.any_instance.stubs(:save_named_map).returns(true)
@user.in_database.run(%{CREATE TABLE xxx (id serial)})
tr = CartoDB::TableRegistrar.new(@user, ::Table)
tr.register('xxx', @external_data_import.data_import_id)
tr.table.tags.should eq tags.join(',') # Tags are stored as a plain string
tr.table.table_visualization.delete
tr.table.destroy
end
it 'creates a default visualization with external source description, attributions and source' do
CartoDB::Visualization::Member.any_instance.stubs(:save_named_map).returns(true)
@user.in_database.run(%{CREATE TABLE xxx (id serial)})
tr = CartoDB::TableRegistrar.new(@user, ::Table)
tr.register('xxx', @external_data_import.data_import_id)
visualization = tr.table.table_visualization
visualization.description.should eq @external_data_import.external_source.visualization.description
visualization.source.should eq @external_data_import.external_source.visualization.source
visualization.attributions.should eq @external_data_import.external_source.visualization.attributions
visualization.delete
tr.table.destroy
end
end
end
File diff suppressed because it is too large Load Diff
+298
View File
@@ -0,0 +1,298 @@
require_relative '../spec_helper'
require 'helpers/unique_names_helper'
describe UserOrganization do
include UniqueNamesHelper
shared_examples 'promoting a user to owner' do
include_context 'visualization creation helpers'
before(:each) do
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
@organization = Organization.new(quota_in_bytes: 1234567890, name: 'wadus', seats: 5).save
@owner = create_user(quota_in_bytes: 524288000, table_quota: 500)
end
after(:each) do
bypass_named_maps
@organization.destroy_cascade if @organization
@owner = ::User.where(id: @owner.id).first if @owner
@owner.destroy if @owner
end
# See #3534: Quota trigger re-creation not done correctly when promoting user to org
it 'moves tables with geometries' do
table = create_random_table(@owner, unique_name('table'))
id = table.id
table.insert_row!(the_geom: %{{"type":"Point","coordinates":[40.392949,-3.69084]}})
table.rows_counted.should == 1
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
table = UserTable.where(id: id).first.service
table.insert_row!({})
table.rows_counted.should == 2
end
# See #3534: Quota trigger re-creation not done correctly when promoting user to org
it 'recreates existing tables triggers' do
table = create_random_table(@owner, unique_name('table'))
id = table.id
table.insert_row!({})
table.rows_counted.should == 1
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
table = UserTable.where(id: id).first.service
table.insert_row!({})
table.rows_counted.should == 2
end
# See #6295: Moving user to its own schema (i.e on org creation) leaves triggers on public schema
it 'moves triggers to the new schema' do
table = create_random_table(@owner, unique_name('table'))
truncate_table_function_creation = %{
create function truncate_table() returns trigger as $truncate_table$
begin
delete from #{table.qualified_table_name};
return NEW;
end;
$truncate_table$ language plpgsql;
}
trigger_name = 'testing_trigger'
@owner.in_database.run(truncate_table_function_creation)
@owner.in_database.run("create trigger #{trigger_name} after insert on #{table.qualified_table_name} execute procedure truncate_table();")
new_row = {}
table.insert_row!(new_row)
table.actual_row_count.should == 0
triggers_before = @owner.db_service.triggers
triggers_before.map(&:trigger_name).should include(trigger_name)
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
triggers_after = @owner.db_service.triggers
triggers_after.map { |t| [t.database_name, t.table_name, t.trigger_name] } .should == triggers_before.map { |t| [t.database_name, t.table_name, t.trigger_name] }
@owner.db_service.triggers('public').should be_empty
end
# See #6295: Moving user to its own schema (i.e on org creation) leaves triggers on public schema
it 'moves functions to the new schema' do
name = 'test_function'
function_creation = %{
create function #{name}() returns integer as $test_function$
begin
return 1;
end;
$test_function$ language plpgsql;
}
@owner.in_database.run(function_creation)
functions_before = @owner.db_service.functions
functions_before.map(&:name).should include(name)
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
functions_after = @owner.db_service.functions
functions_after.map(&:name).should include(name)
@owner.db_service.functions('public').map(&:name).should_not include(name)
end
# See #6642
it 'moves functions with default parameters to the new schema' do
name = 'test_function'
function_creation = %{
create function #{name}(integer default 1) returns integer as $test_function$
begin
return 1;
end;
$test_function$ language plpgsql;
}
@owner.in_database.run(function_creation)
functions_before = @owner.db_service.functions
functions_before.map(&:name).should include(name)
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
functions_after = @owner.db_service.functions
functions_after.map(&:name).should include(name)
@owner.db_service.functions('public').map(&:name).should_not include(name)
end
# See #6295: Moving user to its own schema (i.e on org creation) leaves triggers on public schema
it 'moves views to the new schema' do
table = create_random_table(@owner, unique_name('table'))
view_name = 'mv_test'
create_view_query = %{
CREATE VIEW #{view_name}
AS select name from #{table.qualified_table_name};
}
@owner.in_database.run(create_view_query)
@owner.db_service.views.map(&:name).should include(view_name)
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
@owner.db_service.views.map(&:name).should include(view_name)
@owner.db_service.views('public').map(&:name).should be_empty
end
# See #6295: Moving user to its own schema (i.e on org creation) leaves triggers on public schema
it 'moves materialized views to the new schema' do
table = create_random_table(@owner, unique_name('table'))
materialized_view_name = 'mv_test'
create_materialized_view_query = %{
CREATE MATERIALIZED VIEW #{materialized_view_name}
AS select name from #{table.qualified_table_name};
}
@owner.in_database.run(create_materialized_view_query)
@owner.db_service.materialized_views.map(&:name).should include(materialized_view_name)
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
@owner.db_service.materialized_views.map(&:name).should include(materialized_view_name)
@owner.db_service.materialized_views('public').map(&:name).should be_empty
end
end
# See #5477 Error assigning as owner a user with non-cartodbfied tables
it 'can assign an owner user having non-cartodbfied tables' do
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
@organization = Organization.new(quota_in_bytes: 1234567890, name: 'non-cartodbfied-org', seats: 5).save
@owner = create_user(quota_in_bytes: 524288000, table_quota: 500)
@owner.in_database.run('create table no_cartodbfied_table (test integer)')
@owner.real_tables.count.should == 1
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
owner_org.promote_user_to_admin
@owner.reload
@owner.real_tables.count.should == 1
@owner.database_schema.should == @owner.username
end
it 'keeps tables and metadata if table movement fails' do
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
# This is coupled to DBService#move_tables_to_schema implementation, but we need a way to simulate a failure
Carto::UserTable.stubs(:find_by_user_id_and_name).raises(StandardError.new("Simulation of table movement failure"))
CartoDB::UserModule::DBService.any_instance.stubs(:move_schema_content_by_renaming).raises(StandardError.new("Simulation of table movement failure"))
@organization = Organization.new(quota_in_bytes: 1234567890, name: 'org-that-will-fail', seats: 5).save
@owner = create_user(quota_in_bytes: 524288000, table_quota: 500)
@owner.in_database.run('create table no_cartodbfied_table (test integer)')
# Checks that should also be met afterwards
@owner.database_schema.should == 'public'
@owner.real_tables.count.should == 1
@owner.db_service.schema_exists?(@owner.username).should == false
# Promote
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
expect { owner_org.promote_user_to_admin }.to raise_error StandardError
@owner.reload
@owner.database_schema.should == 'public'
@owner.real_tables.count.should == 1
@owner.db_service.schema_exists?(@owner.username).should == false
end
it 'keeps tables and metadata if rebuild_quota_trigger fails' do
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
@organization = Organization.new(quota_in_bytes: 1234567890, name: 'org-that-will-fail-2', seats: 5).save
@owner = create_user(quota_in_bytes: 524288000, table_quota: 500)
@owner.in_database.run('create table no_cartodbfied_table (test integer)')
# Checks that should also be met afterwards
@owner.database_schema.should == 'public'
@owner.real_tables.count.should == 1
@owner.db_service.schema_exists?(@owner.username).should == false
CartoDB::UserModule::DBService.any_instance.stubs(:rebuild_quota_trigger_with_database).raises(StandardError.new("Failure simulation"))
# Promote
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
expect { owner_org.promote_user_to_admin }.to raise_error StandardError
@owner.reload
@owner.database_schema.should == 'public'
@owner.real_tables.count.should == 1
@owner.db_service.schema_exists?(@owner.username).should == false
end
# Currently, an existing, active organization is the only cause
# that might match #6202 sympthoms, but this is a guess.
it 'keeps tables and metadata if organization is already active' do
::User.any_instance.stubs(:create_in_central).returns(true)
::User.any_instance.stubs(:update_in_central).returns(true)
@owner = create_user(quota_in_bytes: 524288000, table_quota: 500)
@organization = Organization.new(quota_in_bytes: 1234567890, name: 'org-that-will-fail-3', seats: 5, owner_id: @owner.id).save
@owner.in_database.run('create table no_cartodbfied_table (test integer)')
# Checks that should also be met afterwards
@owner.database_schema.should == 'public'
@owner.real_tables.count.should == 1
@owner.db_service.schema_exists?(@owner.username).should == false
# Promote
owner_org = CartoDB::UserOrganization.new(@organization.id, @owner.id)
expect { owner_org.promote_user_to_admin }.to raise_error StandardError
@owner.reload
@owner.database_schema.should == 'public'
@owner.real_tables.count.should == 1
@owner.db_service.schema_exists?(@owner.username).should == false
end
describe 'move_schema_content_step_by_step' do
it_behaves_like 'promoting a user to owner'
before(:each) do
Carto::Db::UserSchemaMover.any_instance.stubs(:default_strategy).returns(Carto::Db::UserSchemaMover::STEPS_STRATEGY)
end
end
end
+290
View File
@@ -0,0 +1,290 @@
require_relative '../spec_helper'
require_relative '../factories/organizations_contexts'
describe Carto::Api::UserPresenter do
before(:each) do
CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true)
end
describe '#current_viewer' do
include_context 'organization with users helper'
it 'displays full data for user administrator' do
presentation = Carto::Api::UserPresenter.new(@carto_org_user_1, current_viewer: @carto_org_user_owner).to_poro
expect(presentation.keys).to include :email
end
it 'displays full data for own user' do
presentation = Carto::Api::UserPresenter.new(@carto_org_user_1, current_viewer: @carto_org_user_1).to_poro
expect(presentation.keys).to include :email
end
it 'displays filtered data for other users' do
presentation = Carto::Api::UserPresenter.new(@carto_org_user_1, current_viewer: @carto_org_user_2).to_poro
expect(presentation.keys).to_not include :email
end
it 'displays filtered data for public' do
presentation = Carto::Api::UserPresenter.new(@carto_org_user_1).to_poro
expect(presentation.keys).to_not include :email
end
end
it "Compares old and new ways of 'presenting' user data" do
Delorean.time_travel_to('2018-01-15')
bypass_named_maps
# Non-org user
user = create_user({
email: 'example@carto.com',
username: 'example',
password: 'example123',
name: "my example name",
sync_tables_enabled: true,
private_tables_enabled: true,
twitter_datasource_enabled: true,
twitter_datasource_block_size: 1000,
twitter_datasource_block_price: 10,
twitter_datasource_quota: 70000,
soft_twitter_datasource_limit: true,
public_visualization_count: 1,
all_visualization_count: 2,
job_role: "Developer",
company: "test",
phone: "123",
industry: "Academic and Education",
period_end_date: Time.parse('2018-01-01')
})
# Some sample data
data_import_id = '11111111-1111-1111-1111-111111111111'
SequelRails.connection.run(%Q{
INSERT INTO data_imports("data_source","data_type","table_name","state","success","logger","updated_at",
"created_at","tables_created_count",
"table_names","append","id","table_id","user_id",
"service_name","service_item_id","stats","type_guessing","quoted_fields_guessing","content_guessing","server","host",
"resque_ppid","upload_host","create_visualization","user_defined_limits")
VALUES('test','url','test','complete','t','11111111-1111-1111-1111-111111111112',
'2015-03-17 00:00:00.94006+00','2015-03-17 00:00:00.810581+00','1',
'test','f','#{data_import_id}','11111111-1111-1111-1111-111111111113',
'#{user.id}','public_url', 'test',
'[{"type":".csv","size":5015}]','t','f','t','test','0.0.0.0','13204','test','f','{"twitter_credits_limit":0}');
})
SequelRails.connection.run(%Q{
INSERT INTO geocodings("table_name","processed_rows","created_at","updated_at","formatter","state",
"id","user_id",
"cache_hits","kind","geometry_type","processable_rows","real_rows","used_credits",
"data_import_id"
) VALUES('importer_123456','197','2015-03-17 00:00:00.279934+00','2015-03-17 00:00:00.536383+00','field_1','finished',
'11111111-1111-1111-1111-111111111114','#{user.id}','0','admin0','polygon','195','0','0',
'#{data_import_id}');
})
create_table( { user_id: user.id, name: 'table1' } )
create_table( { user_id: user.id, name: 'table2', privacy: Carto::UserTable::PRIVACY_PUBLIC } )
feature_flag1 = FactoryGirl.create(:feature_flag, id: 1, name: 'ff1')
feature_flag2 = FactoryGirl.create(:feature_flag, id: 2, name: 'ff2')
user.set_relationships_from_central({ feature_flags: [ feature_flag1.id.to_s, feature_flag2.id.to_s ]})
user.save.reload
compare_data(user.data, Carto::Api::UserPresenter.new(Carto::User.where(id: user.id).first).data, false, false)
# Now org user, organization and another member
owner = create_user({
email: 'owner@carto.com',
username: 'owner',
password: 'owner123',
name: "owner name",
sync_tables_enabled: true,
private_tables_enabled: true,
twitter_datasource_enabled: true,
twitter_datasource_block_size: 1000,
twitter_datasource_block_price: 10,
twitter_datasource_quota: 70000,
soft_twitter_datasource_limit: true,
public_visualization_count: 1,
all_visualization_count: 2,
job_role: "Developer",
company: "test",
phone: "123",
industry: "Academic and Education",
period_end_date: Time.parse('2018-01-01')
})
organization = ::Organization.new(quota_in_bytes: 200.megabytes, name: 'testorg', seats: 5).save
user_org = CartoDB::UserOrganization.new(organization.id, owner.id)
user_org.promote_user_to_admin
organization.reload
owner.reload
user2 = create_user({
email: 'example2@carto.com',
username: 'example2',
password: 'example123',
account_type: 'ORGANIZATION USER'
})
user2.organization = organization
user2.save
user2.reload
organization.reload
compare_data(owner.data, Carto::Api::UserPresenter.new(Carto::User.where(id: owner.id).first).data, true)
SequelRails.connection.run( %Q{ DELETE FROM geocodings } )
SequelRails.connection.run( %Q{ DELETE FROM data_imports } )
user.destroy
organization.destroy
Delorean.back_to_the_present
end
protected
def compare_data(original_old_data, new_data, org_user = false, mobile_sdk_enabled = false)
old_data = add_new_keys(original_old_data)
# INFO: new organization presenter now doesn't contain users
old_data[:organization].delete(:users) if old_data[:organization]
# TODO: This fails at CI server, until there's time to research...
#new_data.should eq old_data
# To detect deltas not migrated to new presenter
new_data.keys.sort.should == old_data.keys.sort
new_data[:id].should == old_data[:id]
new_data[:email].should == old_data[:email]
new_data[:name].should == old_data[:name]
new_data[:username].should == old_data[:username]
new_data[:account_type].should == old_data[:account_type]
new_data[:table_quota].should == old_data[:table_quota]
new_data[:public_map_quota].should == old_data[:public_map_quota]
new_data[:regular_api_key_quota].should == old_data[:regular_api_key_quota]
new_data[:table_count].should == old_data[:table_count]
new_data[:public_visualization_count].should == old_data[:public_visualization_count]
new_data[:public_privacy_visualization_count].should == old_data[:public_privacy_visualization_count]
new_data[:link_privacy_visualization_count].should == old_data[:link_privacy_visualization_count]
new_data[:password_privacy_visualization_count].should == old_data[:password_privacy_visualization_count]
new_data[:all_visualization_count].should == old_data[:all_visualization_count]
new_data[:visualization_count].should == old_data[:visualization_count]
new_data[:failed_import_count].should == old_data[:failed_import_count]
new_data[:success_import_count].should == old_data[:success_import_count]
new_data[:import_count].should == old_data[:import_count]
new_viz_date = new_data[:last_visualization_created_at].to_s
old_viz_date = old_data[:last_visualization_created_at].to_s
Time.parse(new_viz_date).should eq Time.parse(old_viz_date) unless old_viz_date.blank? && new_viz_date.blank?
new_data[:quota_in_bytes].should == old_data[:quota_in_bytes]
new_data[:db_size_in_bytes].should == old_data[:db_size_in_bytes]
new_data[:db_size_in_megabytes].should == old_data[:db_size_in_megabytes]
new_data[:remaining_table_quota].should == old_data[:remaining_table_quota]
new_data[:remaining_byte_quota].should == old_data[:remaining_byte_quota]
new_data[:api_calls].should == old_data[:api_calls]
new_data[:api_calls_quota].should == old_data[:api_calls_quota]
new_data[:api_calls_block_price].should == old_data[:api_calls_block_price]
new_data[:geocoding].should == old_data[:geocoding]
new_data[:here_isolines].should == old_data[:here_isolines]
new_data[:obs_snapshot].should == old_data[:obs_snapshot]
new_data[:obs_general].should == old_data[:obs_general]
new_data[:twitter].should == old_data[:twitter]
new_data[:billing_period].should == old_data[:billing_period]
new_data[:next_billing_period].should == old_data[:next_billing_period]
new_data[:max_layers].should == old_data[:max_layers]
new_data[:api_key].should == old_data[:api_key]
new_data[:layers].should == old_data[:layers]
new_data[:trial_ends_at].should == old_data[:trial_ends_at]
new_data[:upgraded_at].should == old_data[:upgraded_at]
new_data[:show_trial_reminder].should == old_data[:show_trial_reminder]
new_data[:show_upgraded_message].should == old_data[:show_upgraded_message]
new_data[:actions].should == old_data[:actions]
new_data[:notification].should == old_data[:notification]
new_data[:avatar_url].should == old_data[:avatar_url]
new_data[:new_dashboard_enabled].should == old_data[:new_dashboard_enabled]
new_data[:feature_flags].should == old_data[:feature_flags]
new_data[:base_url].should == old_data[:base_url]
new_data[:geocoder_provider].should == old_data[:geocoder_provider]
new_data[:isolines_provider].should == old_data[:isolines_provider]
new_data[:routing_provider].should == old_data[:routing_provider]
new_data[:mfa_configured].should == old_data[:mfa_configured]
if org_user
new_data[:organization].keys.sort.should == old_data[:organization].keys.sort
# This is an implicit test of OrganizationPresenter...
# INFO: we have a weird error sometimes running builds that fails comparing dates despite having equal value...
# > Diff:2015-06-23 17:27:02 +0200.==(2015-06-23 17:27:02 +0200) returned false even though the diff between
# 2015-06-23 17:27:02 +0200 and 2015-06-23 17:27:02 +0200 is empty. Check the implementation of
# 2015-06-23 17:27:02 +0200.==.
new_data[:organization][:created_at].should == old_data[:organization][:created_at]
new_data[:organization][:description].should == old_data[:organization][:description]
new_data[:organization][:discus_shortname].should == old_data[:organization][:discus_shortname]
new_data[:organization][:display_name].should == old_data[:organization][:display_name]
new_data[:organization][:id].should == old_data[:organization][:id]
new_data[:organization][:name].should == old_data[:organization][:name]
new_data[:organization][:owner][:id].should == old_data[:organization][:owner][:id]
new_data[:organization][:owner][:username].should == old_data[:organization][:owner][:username]
new_data[:organization][:owner][:avatar_url].should == old_data[:organization][:owner][:avatar_url]
new_data[:organization][:owner][:email].should == old_data[:organization][:owner][:email]
new_data[:organization][:quota_in_bytes].should == old_data[:organization][:quota_in_bytes]
new_data[:organization][:geocoding_quota].should == old_data[:organization][:geocoding_quota]
new_data[:organization][:here_isolines_quota].should == old_data[:organization][:here_isolines_quota]
new_data[:organization][:obs_snapshot_quota].should == old_data[:organization][:obs_snapshot_quota]
new_data[:organization][:obs_general_quota].should == old_data[:organization][:obs_general_quota]
new_data[:organization][:mapzen_routing_quota].should == old_data[:organization][:mapzen_routing_quota]
new_data[:organization][:map_view_quota].should == old_data[:organization][:map_view_quota]
new_data[:organization][:twitter_datasource_quota].should == old_data[:organization][:twitter_datasource_quota]
new_data[:organization][:map_view_block_price].should == old_data[:organization][:map_view_block_price]
new_data[:organization][:geocoding_block_price].should == old_data[:organization][:geocoding_block_price]
new_data[:organization][:here_isolines_block_price].should == old_data[:organization][:here_isolines_block_price]
new_data[:organization][:obs_snapshot_block_price].should == old_data[:organization][:obs_snapshot_block_price]
new_data[:organization][:obs_general_block_price].should == old_data[:organization][:obs_general_block_price]
new_data[:organization][:mapzen_routing_block_price].should == old_data[:organization][:mapzen_routing_block_price]
new_data[:organization][:seats].should == old_data[:organization][:seats]
new_data[:organization][:twitter_username].should == old_data[:organization][:twitter_username]
new_data[:organization][:location].should == old_data[:organization][:location]
# Same as [:organization][:created_at] issue above
# TODO Skipped organization.created_at due to Rails 4 TZ issues
# new_data[:organization][:updated_at].to_s.should == old_data[:organization][:updated_at].to_s
#owner is excluded from the users list
new_data[:organization][:website].should == old_data[:organization][:website]
new_data[:organization][:avatar_url].should == old_data[:organization][:avatar_url]
new_data[:geocoder_provider].should == old_data[:geocoder_provider]
new_data[:isolines_provider].should == old_data[:isolines_provider]
new_data[:routing_provider].should == old_data[:routing_provider]
end
if mobile_sdk_enabled
new_data[:mobile_apps].keys.sort.should == old_data[:mobile_apps].keys.sort
new_data[:mobile_apps][:mobile_xamarin].should = new_data[:mobile_apps].mobile_xamarin
new_data[:mobile_apps][:mobile_custom_watermark].should = new_data[:mobile_apps].mobile_custom_watermark
new_data[:mobile_apps][:mobile_offline_maps].should = new_data[:mobile_apps].mobile_offline_maps
new_data[:mobile_apps][:mobile_gis_extension].should = new_data[:mobile_apps].mobile_gis_extension
new_data[:mobile_apps][:mobile_max_open_users].should = new_data[:mobile_apps].mobile_max_open_users
new_data[:mobile_apps][:mobile_max_private_users].should = new_data[:mobile_apps].mobile_max_private_users
end
# TODO: Pending migration and testing of :real_table_count & :last_active_time
end
def add_new_keys(user_poro)
new_poro = user_poro.dup.deep_merge(viewer: false)
new_poro[:organization] = user_poro[:organization].deep_merge(viewer_seats: 0) if user_poro[:organization].present?
new_poro[:mfa_configured] = false
new_poro[:next_billing_period] = Time.parse('2018-02-01')
new_poro
end
def create_org(org_name, org_quota, org_seats)
organization = Organization.new
organization.name = org_name
organization.quota_in_bytes = org_quota
organization.seats = org_seats
organization.save!
organization
end
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+167
View File
@@ -0,0 +1,167 @@
shared_examples_for 'user table models' do
describe '#validation' do
after(:each) do
@duplicate.try(:destroy)
end
it 'validates that user is required' do
ut = build_user_table
expect(ut.valid?).to be_false
# Error key is :user_id for Sequel, :user for AR
expect(ut.errors.keys & [:user, :user_id]).not_to be_empty
end
it 'validates that viewer user cannot modify tables' do
@user.stubs(:viewer).returns(true)
ut = build_user_table(user: @user)
expect(ut.valid?).to be_false
expect(ut.errors.keys).to include :user
end
it 'validates that name cannot be a reserved name' do
ut = build_user_table(user: @user, name: 'public')
expect(ut.valid?).to be_false
expect(ut.errors.keys).to include :name
end
it 'validates that name cannot be taken' do
@duplicate = FactoryGirl.create(:carto_user_table, user: Carto::User.find(@user.id), name: 'ut_spec_wadus')
ut = build_user_table(user: @user, name: 'ut_spec_wadus')
expect(ut.valid?).to be_false
expect(ut.errors.keys.flatten).to include :name
end
it 'validates privacy' do
begin
ut = build_user_table(user: @user, privacy: 234)
# AR fails validation
expect(ut.valid?).to be_false
expect(ut.errors.keys).to include :privacy
rescue RuntimeError => e
# Sequel raises when building the model
raise 'Spec failed' unless e.message.include?('Invalid privacy value')
end
end
it 'defaults to public privacy for users without private tables' do
@user.stubs(:private_tables_enabled).returns(false)
ut = build_user_table(user: @user)
expect(ut.valid?).to be_true
expect(ut.privacy).to eq Carto::UserTable::PRIVACY_PUBLIC
end
it 'defaults to private privacy for users with private tables' do
@user.stubs(:private_tables_enabled).returns(true)
ut = build_user_table(user: @user)
expect(ut.valid?).to be_true
expect(ut.privacy).to eq Carto::UserTable::PRIVACY_PRIVATE
end
it 'validates privacy option for user without private tables' do
@user.stubs(:private_tables_enabled).returns(false)
ut = build_user_table(user: @user, privacy: Carto::UserTable::PRIVACY_PUBLIC)
expect(ut.valid?).to be_true
ut = build_user_table(user: @user, privacy: Carto::UserTable::PRIVACY_PRIVATE)
expect(ut.valid?).to be_false
expect(ut.errors.keys).to include :privacy
end
it 'validates privacy option for user with private tables' do
@user.stubs(:private_tables_enabled).returns(true)
ut = build_user_table(user: @user, privacy: Carto::UserTable::PRIVACY_PUBLIC)
expect(ut.valid?).to be_true
ut = build_user_table(user: @user, privacy: Carto::UserTable::PRIVACY_PRIVATE)
expect(ut.valid?).to be_true
end
end
describe '#estimated_row_count and #actual_row_count' do
it 'should query Table estimated an actual row count methods' do
::Table.any_instance.stubs(:estimated_row_count).returns(999)
::Table.any_instance.stubs(:actual_row_count).returns(1000)
@user_table.estimated_row_count.should == 999
@user_table.actual_row_count.should == 1000
end
end
it 'should sync table_id with physical table oid' do
@user_table.table_id = nil
@user_table.save
@user_table.table_id.should be_nil
@user_table.sync_table_id.should eq @user_table.service.get_table_id
end
describe 'dependent visualizations' do
include Carto::Factories::Visualizations
before(:all) do
@map, @other_table, @table_visualization, @visualization = create_full_visualization(@carto_user)
@first_layer = @visualization.data_layers.first
@second_layer = FactoryGirl.create(:carto_layer, kind: 'torque', maps: [@map])
end
before(:each) do
@second_layer.update_attribute(:kind, 'torque')
@user_table.reload
end
after(:all) do
@second_layer.destroy
destroy_full_visualization(@map, @other_table, @table_visualization, @visualization)
end
it 'no layers depending on the table -> not dependent' do
@first_layer.user_table_ids = []
@second_layer.user_table_ids = []
@dependent_test_object.fully_dependent_visualizations.any?.should be_false
@dependent_test_object.partially_dependent_visualizations.any?.should be_false
end
it 'one layer depending on the table -> fully dependent' do
@first_layer.user_table_ids = [@user_table.id]
@second_layer.user_table_ids = []
@second_layer.update_attribute(:kind, 'tiled')
@dependent_test_object.fully_dependent_visualizations.any?.should be_true
@dependent_test_object.partially_dependent_visualizations.any?.should be_false
end
it 'two layers depending on the table -> fully dependent' do
@first_layer.user_table_ids = [@user_table.id]
@second_layer.user_table_ids = [@user_table.id]
@dependent_test_object.fully_dependent_visualizations.any?.should be_true
@dependent_test_object.partially_dependent_visualizations.any?.should be_false
end
it 'two layers depending on different tables -> partially dependent' do
@first_layer.user_table_ids = [@user_table.id]
@second_layer.user_table_ids = [@other_table.id]
@dependent_test_object.fully_dependent_visualizations.any?.should be_false
@dependent_test_object.partially_dependent_visualizations.any?.should be_true
end
it 'two layers depending on multiple tables (both include this one) -> fully dependent' do
@first_layer.user_table_ids = [@user_table.id]
@second_layer.user_table_ids = [@user_table.id, @other_table.id]
@dependent_test_object.fully_dependent_visualizations.any?.should be_true
@dependent_test_object.partially_dependent_visualizations.any?.should be_false
end
it 'two layers depending on multiple tables (this table is only used in one layer) -> partially dependent' do
@first_layer.user_table_ids = [@user_table.id, @other_table.id]
@second_layer.user_table_ids = [@other_table.id]
@dependent_test_object.fully_dependent_visualizations.any?.should be_false
@dependent_test_object.partially_dependent_visualizations.any?.should be_true
end
end
end
+82
View File
@@ -0,0 +1,82 @@
require_relative '../spec_helper'
require 'models/user_table_shared_examples'
describe UserTable do
before(:all) do
bypass_named_maps
@user = create_user(email: 'admin@cartotest.com', username: 'admin', password: '000123456')
@carto_user = Carto::User.find(@user.id)
@user_table = ::UserTable.new
@user_table.user_id = @user.id
@user_table.name = 'user_table'
@user_table.save
# The dependent visualization models are in the Table class for the Sequel model
@dependent_test_object = @user_table.service
end
after(:all) do
@user_table.destroy
@user.destroy
end
it_behaves_like 'user table models' do
def build_user_table(attrs = {})
::UserTable.new.set_all(attrs)
end
end
it "can save large OIDs" do
user_table = ::UserTable.new
user_table.user_id = @user.id
user_table.name = 'user_table_3'
user_table.save
user_table.reload
user_table.table_id = 2**32 - 1
user_table.save
user_table.reload
user_table.table_id.should eq 2**32 - 1
user_table.destroy
end
context 'viewer users' do
after(:each) do
@user.viewer = false
@user.save
end
it "can't create new user tables" do
bypass_named_maps
@user.viewer = true
@user.save
@user_table = ::UserTable.new
@user_table.user_id = @user.id
@user_table.name = 'user_table_2'
expect { @user_table.save }.to raise_error(Sequel::ValidationFailed, /Viewer users can't create tables/)
end
it "can't delete user tables" do
bypass_named_maps
@user_table = ::UserTable.new
@user_table.user_id = @user.id
@user_table.name = 'user_table_2'
@user_table.save
@user.viewer = true
@user.save
@user_table.reload
expect { @user_table.destroy }.to raise_error(CartoDB::InvalidMember, /Viewer users can't destroy tables/)
@user.viewer = false
@user.save
@user_table.reload
@user_table.destroy
end
end
end
@@ -0,0 +1,478 @@
require_relative '../../spec_helper'
require_relative '../../../services/data-repository/backend/sequel'
require_relative '../../../services/data-repository/repository'
require_relative '../../../app/models/visualization/collection'
require_relative '../../../app/models/visualization/member'
require_relative '../../doubles/support_tables.rb'
require 'helpers/unique_names_helper'
include UniqueNamesHelper
include CartoDB
describe Visualization::Collection do
include Carto::Factories::Visualizations
before(:all) do
@user_1 = FactoryGirl.create(:valid_user, quota_in_bytes: 524288000, table_quota: 500, private_tables_enabled: true)
@user_2 = FactoryGirl.create(:valid_user, private_tables_enabled: true)
end
before(:each) do
bypass_named_maps
delete_user_data @user_1
delete_user_data @user_2
end
after(:all) do
@user_1.destroy
@user_2.destroy
end
describe '#fetch' do
it 'filters by tag if the backend supports array columns' do
attributes1 = random_attributes_for_vis_member(tags: ['tag 1', 'tag 11'], user_id: @user_1.id)
attributes2 = random_attributes_for_vis_member(tags: ['tag 2', 'tag 22'], user_id: @user_1.id)
Visualization::Member.new(attributes1).store
Visualization::Member.new(attributes2).store
collection = Visualization::Collection.new
collection.fetch(user_id: @user_1.id, tags: 'tag 1').count.should == 1
end
it 'filters by partial name / description match' do
attributes1 =
random_attributes_for_vis_member(name: 'viz_1', description: 'description_11', user_id: @user_1.id)
attributes2 =
random_attributes_for_vis_member(name: 'viz_2', description: 'description_22', user_id: @user_1.id)
Visualization::Member.new(attributes1).store
Visualization::Member.new(attributes2).store
collection = Visualization::Collection.new
collection.fetch(user_id: @user_1.id, q: 'viz').count.should == 2
collection.fetch(user_id: @user_1.id, q: 'viz_1').count.should == 1
collection = Visualization::Collection.new
collection.fetch(user_id: @user_1.id, q: 'description').count.should == 2
collection.fetch(user_id: @user_1.id, q: 'ion_11').count.should == 1
collection.fetch(user_id: @user_1.id, q: 'ion_22').count.should == 1
end
it 'orders the collection by the passed criteria' do
Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_1.id, name: 'viz_1')).store
Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_1.id, name: 'viz_2')).store
collection = Visualization::Collection.new
records = collection.fetch(user_id: @user_1.id, order: :name)
records.first.name.should == 'viz_2'
end
it 'checks fetching with, without and only shared entities' do
vis_1_name = unique_name('viz')
vis_2_name = unique_name('viz')
vis_3_name = unique_name('viz')
Visualization::Member.new(random_attributes_for_vis_member(name: vis_1_name, user_id: @user_1.id)).store
vis2 = Visualization::Member.new(random_attributes_for_vis_member(name: vis_2_name, user_id: @user_2.id)).store
vis3 = Visualization::Member.new(random_attributes_for_vis_member(name: vis_3_name, user_id: @user_2.id)).store
shared_entity = CartoDB::SharedEntity.new(
recipient_id: @user_1.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: vis2.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
)
shared_entity.save
shared_entity.reload
collection = Visualization::Collection.new
collection.stubs(:user_shared_vis).with(@user_1.id).returns([vis2.id])
# Filter by user_id and non-owned id (excluding shared)
records = collection.fetch(user_id: @user_1.id, id: vis3.id, exclude_shared: true)
records.count.should eq 0
# Filter by user_id and non-owned id (not excluding shared)
records = collection.fetch(user_id: @user_1.id, id: vis3.id)
records.count.should eq 0
# Filter by name (not present user_id)
records = collection.fetch(name: vis_1_name)
records.count.should eq 1
records.first.name.should eq vis_1_name
# Filter by user_id (includes shared)
records = collection.fetch(user_id: @user_1.id).map { |item| item }
records.count.should eq 2
((records[0].name == vis_1_name || records[0].name == vis_2_name) &&
(records[1].name == vis_1_name || records[1].name == vis_2_name)).should eq true
# Filter by user_id excluding shared
records = collection.fetch(user_id: @user_1.id, exclude_shared: true)
records.count.should eq 1
records.first.name.should eq vis_1_name
# Filter by user_id and only shared
records = collection.fetch(user_id: @user_1.id, only_shared: true)
records.count.should eq 1
records.first.name.should eq vis_2_name
end
it 'checks that filtering collection by locked works' do
collection = Visualization::Collection.new
vis1 = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_1.id,
name: 'viz_1',
locked: true)).store
vis2 = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_1.id,
name: 'viz_2',
locked: false)).store
records = collection.fetch(user_id: @user_1.id, locked: false)
records.count.should eq 1
records.first.name.should eq vis2.name
records = collection.fetch(user_id: @user_1.id, locked: true)
records.count.should eq 1
records.first.name.should eq vis1.name
end
it "checks that shared entities appear no matter if they're locked or not" do
vis_1_name = 'viz_1'
vis_2_name = 'viz_2'
vis_3_name = 'viz_3'
vis_4_name = 'viz_4'
Visualization::Member.new(random_attributes_for_vis_member(name: vis_1_name,
user_id: @user_1.id,
locked: true)).store
vis2 = Visualization::Member.new(random_attributes_for_vis_member(name: vis_2_name,
user_id: @user_2.id,
locked: true)).store
vis3 = Visualization::Member.new(random_attributes_for_vis_member(name: vis_3_name,
user_id: @user_2.id,
locked: false)).store
Visualization::Member.new(random_attributes_for_vis_member(name: vis_4_name,
user_id: @user_1.id,
locked: false)).store
shared_entity = CartoDB::SharedEntity.new(
recipient_id: @user_1.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: vis2.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
)
shared_entity.save
shared_entity = CartoDB::SharedEntity.new(
recipient_id: @user_1.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: vis3.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
)
shared_entity.save
collection = Visualization::Collection.new
collection.stubs(:user_shared_vis).with(@user_1.id).returns([vis2.id, vis3.id])
# Non-locked vis, all shared vis
records = collection.fetch(user_id: @user_1.id)
records.count.should eq 4
# Same behaviour, non-locked, all shared
records = collection.fetch(user_id: @user_1.id, locked: false)
records.count.should eq 3
# Only user vis, no shared vis at all
records = collection.fetch(user_id: @user_1.id, locked: true)
records.count.should eq 1
records.map(&:name).first.should eq vis_1_name
end
it 'Checks all supported sorting methods work' do
# Supported: updated_at, likes, mapviews, row_count, size
# TODO: Add mapviews test. As it uses redis requires more work
vis1 = full_visualization_table(@user_1, nil).visualization
vis2 = full_visualization_table(@user_1, nil).visualization
vis3 = full_visualization_table(@user_1, nil).visualization
# Biggest row count
vis3.table.add_column!(name: "test_col", type: "text")
vis3.table.insert_row!(test_col: "333")
vis3.table.insert_row!(test_col: "333")
vis3.table.insert_row!(test_col: "333")
vis3.table.insert_row!(test_col: "333")
vis3.table.insert_row!(test_col: "333")
vis3.table.insert_row!(test_col: "333")
# pg_class.reltuples only get updated after VACUUMs, etc.
@user_1.in_database.run("VACUUM #{vis3.table.name}")
vis3.updated_at = Time.now - 3.minute
vis3.save
# Biggest in size and likes
long_string = ""
2000.times do
long_string << rand(999).to_s
end
vis2.table.add_column!(name: "test_col", type: "text")
vis2.table.add_column!(name: "test_col2", type: "text")
vis2.table.add_column!(name: "test_col3", type: "text")
vis2.table.insert_row!(test_col: long_string, test_col2: long_string, test_col3: long_string)
vis2.table.insert_row!(test_col: long_string, test_col2: long_string, test_col3: long_string)
vis2.table.insert_row!(test_col: long_string, test_col2: long_string, test_col3: long_string)
vis2.table.insert_row!(test_col: long_string, test_col2: long_string, test_col3: long_string)
@user_1.in_database.run("VACUUM #{vis2.table.name}")
vis2.updated_at = Time.now - 2.minute
vis2.save
# Latest edited
vis1.table.add_column!(name: "test_col", type: "text")
@user_1.in_database.run("VACUUM #{vis1.table.name}")
vis1.updated_at = Time.now - 1.minute
vis1.save
# Actual tests start here
collection = Visualization::Collection.new.fetch(user_id: @user_1.id,
order: 'updated_at',
exclude_shared: true)
collection.count.should eq 3
ids = collection.map(&:id)
expected_updated_ats = [vis1.id, vis2.id, vis3.id]
ids.should eq expected_updated_ats
collection = Visualization::Collection.new.fetch(user_id: @user_1.id,
order: :row_count,
exclude_shared: true)
collection.count.should eq 3
ids = collection.map(&:id)
expected_row_count = [vis3.id, vis2.id, vis1.id]
ids.should eq expected_row_count
collection = Visualization::Collection.new.fetch(user_id: @user_1.id,
order: :size,
exclude_shared: true)
collection.count.should eq 3
ids = collection.map(&:id)
expected_size = [vis2.id, vis3.id, vis1.id]
ids.should eq expected_size
# Cleanup
vis1.delete
vis2.delete
vis3.delete
end
def liked(user)
Visualization::Collection.new.fetch(user_id: user.id, only_liked: true)
end
it "checks filtering by 'liked' " do
user3 = create_user(quota_in_bytes: 524288000, table_quota: 500, private_tables_enabled: true)
full_visualization_table(@user_1, nil).visualization
vis2 = full_visualization_table(@user_1, nil).visualization
vis2.privacy = Visualization::Member::PRIVACY_PUBLIC
vis2.save
vis3 = full_visualization_table(@user_1, nil).visualization
vis3.privacy = Visualization::Member::PRIVACY_PUBLIC
vis3.save
vis4 = full_visualization_table(@user_1, nil).visualization
vis4.privacy.should eq Visualization::Member::PRIVACY_PRIVATE
vis_link = full_visualization_table(@user_1, nil).visualization
vis_link.privacy = Visualization::Member::PRIVACY_LINK
vis_link.save
vis_private = full_visualization_table(@user_1, nil).visualization
vis_private.privacy = Visualization::Member::PRIVACY_PRIVATE
vis_private.save
# vis1 0 likes
vis2.add_like_from(@user_1)
expect {
vis2.add_like_from(@user_2)
}.to raise_exception Carto::Visualization::UnauthorizedLikeError
vis3.add_like_from(@user_1)
# since vis4 is not public it won't count for users 2 and 3
vis4.add_like_from(@user_1)
vis_link.add_like_from(@user_1)
vis_private.add_like_from(@user_1)
collection = Visualization::Collection.new.fetch(user_id: @user_1.id)
collection.count.should eq 6
collection = Visualization::Collection.new.fetch(user_id: @user_1.id, only_liked: true)
collection.count.should eq 5
ids = collection.map(&:id)
collection = Visualization::Collection.new.fetch(user_id: @user_1.id,
type: Visualization::Member::TYPE_CANONICAL,
only_liked: true)
collection.count.should eq 5
ids = collection.map(&:id)
collection = Visualization::Collection.new.fetch(user_id: @user_1.id,
only_liked: true,
unauthenticated: true)
collection.count.should eq 2
ids = collection.map(&:id)
collection = Visualization::Collection.new.fetch(user_id: @user_1.id,
only_liked: true,
privacy: Visualization::Member::PRIVACY_PRIVATE)
collection.count.should eq 2
ids = collection.map(&:id)
collection = Visualization::Collection.new.fetch(only_liked: true)
collection.count.should eq 0
collection = Visualization::Collection.new.fetch(user_id: @user_2.id, only_liked: true)
collection.count.should eq 0
collection = Visualization::Collection.new.fetch(user_id: user3.id, only_liked: true)
collection.count.should eq 0 # Liked link privacy one
user3.destroy
end
end
describe 'single methods' do
it 'checks count_total method' do
vis_1_name = 'viz_1'
vis_2_name = 'viz_2'
vis_3_name = 'viz_3'
vis_4_name = 'viz_4'
Visualization::Member.new(random_attributes(name: vis_1_name, user_id: @user_1.id)).store
Visualization::Member.new(random_attributes(name: vis_2_name, privacy: 'private', user_id: @user_1.id)).store
vis_user2 = Visualization::Member.new(random_attributes(name: vis_3_name, user_id: @user_2.id)).store
vis2_user2 = Visualization::Member.new(random_attributes(name: vis_4_name, user_id: @user_2.id)).store
shared_entity = CartoDB::SharedEntity.new(
recipient_id: @user_1.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: vis_user2.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
)
shared_entity.save
shared_entity.reload
collection = Visualization::Collection.new
collection.stubs(:user_shared_vis).with(@user_1.id).returns([vis_user2.id])
records = collection.fetch(user_id: @user_1.id)
records.count.should eq 3
collection.count_total(user_id: @user_1.id).should eq 2
# Other filters should be skipped
collection.count_total(user_id: @user_1.id, id: vis_user2.id).should eq 2
collection.count_total(user_id: @user_1.id, id: vis2_user2.id).should eq 2
collection.count_total(user_id: @user_1.id, name: 'test').should eq 2
collection.count_total(user_id: @user_1.id, description: 'test').should eq 2
collection.count_total(user_id: @user_1.id, privacy: CartoDB::Visualization::Member::PRIVACY_PRIVATE).should eq 2
collection.count_total(user_id: @user_1.id, locked: true).should eq 2
collection.count_total(user_id: @user_1.id, exclude_shared: false).should eq 2
collection.count_total(user_id: @user_1.id, only_shared: false).should eq 2
# If unauthenticated remove private ones
collection.count_total(user_id: @user_1.id, unauthenticated: true).should eq 1
# Type filters are allowed
collection.count_total(user_id: @user_1.id, type: CartoDB::Visualization::Member::TYPE_CANONICAL).should eq 2
collection.count_total(user_id: @user_1.id, type: CartoDB::Visualization::Member::TYPE_DERIVED).should eq 0
# And filtering by user_id
collection.count_total(user_id: @user_2.id).should eq 2
end
end
# Slide visualization types specs
# This should be a member_spec test, but as those specs have no collection support...
it 'checks the .children method' do
parent = Visualization::Member.new(random_attributes_for_vis_member(
user_id: @user_1.id,
type: Visualization::Member::TYPE_DERIVED)).store
parent.children.count.should eq 0
child1 = Visualization::Member.new(random_attributes_for_vis_member(
user_id: @user_1.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: parent.id)).store.fetch
parent.children.count.should eq 1
child2 = Visualization::Member.new(random_attributes_for_vis_member(
user_id: @user_1.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: parent.id)).store.fetch
child2.set_prev_list_item!(child1)
parent.fetch
parent.children.count.should eq 2
canonical = Visualization::Member.new(random_attributes_for_vis_member(
user_id: @user_1.id,
type: Visualization::Member::TYPE_CANONICAL)).store
parent.children.count.should eq 2
canonical.children.count.should eq 0
child_2_1 = Visualization::Member.new(random_attributes_for_vis_member(
user_id: @user_1.id,
type: Visualization::Member::TYPE_DERIVED)).store
Visualization::Member.new(random_attributes_for_vis_member(
user_id: @user_1.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: child_2_1.id)).store
child_2_1.children.count.should eq 1
end
it 'checks retrieving slide type items' do
member = Visualization::Member.new(random_attributes_for_vis_member(
type: Visualization::Member::TYPE_DERIVED,
user_id: @user_1.id)).store
c1 = Visualization::Member.new(random_attributes_for_vis_member(
type: Visualization::Member::TYPE_SLIDE,
parent_id: member.id,
user_id: @user_1.id)).store
c2 = Visualization::Member.new(random_attributes_for_vis_member(
type: Visualization::Member::TYPE_SLIDE,
parent_id: member.id,
user_id: @user_1.id)).store
collection = Visualization::Collection.new.fetch(user_id: @user_1.id,
type: Visualization::Member::TYPE_SLIDE)
collection.count.should eq 2
c1.delete
c2.delete
member.delete
end
def random_attributes(attributes = {})
random = unique_name('viz')
{
name: attributes.fetch(:name, random),
description: attributes.fetch(:description, "description #{random}"),
privacy: attributes.fetch(:privacy, 'public'),
tags: attributes.fetch(:tags, ['tag 1']),
type: attributes.fetch(:type, CartoDB::Visualization::Member::TYPE_CANONICAL),
user_id: attributes.fetch(:user_id, UUIDTools::UUID.timestamp_create.to_s),
locked: attributes.fetch(:locked, false),
title: attributes.fetch(:title, ''),
source: attributes.fetch(:source, ''),
license: attributes.fetch(:license, ''),
attributions: attributes.fetch(:attributions, ''),
kind: attributes.fetch(:kind, CartoDB::Visualization::Member::KIND_GEOM),
map_id: attributes.fetch(:map_id, nil)
}
end # random_attributes
end # Visualization::Collection
+117
View File
@@ -0,0 +1,117 @@
require 'rspec/core'
require 'rspec/expectations'
require 'rspec/mocks'
require 'sequel'
require_relative '../../spec_helper'
require_relative '../../../app/models/visualization/locator'
require_relative '../../../app/models/visualization'
require_relative '../../../app/models/visualization/member'
require_relative '../../doubles/support_tables.rb'
include CartoDB
describe Visualization::Locator do
UUID = 'db0dfb0c-a944-11e3-a51e-30f9edfe5da6'
before(:each) do
support_tables_mock = Doubles::Visualization::SupportTables.new
Visualization::Relator.any_instance.stubs(:support_tables).returns(support_tables_mock)
end
before do
bypass_named_maps
@db = SequelRails.connection
Sequel.extension(:pagination)
Visualization.repository = DataRepository::Backend::Sequel.new(@db, :visualizations)
@user_id = UUIDTools::UUID.timestamp_create.to_s
@map_id = UUIDTools::UUID.timestamp_create.to_s
# For relator->permission
@user_mock = create_mocked_user
CartoDB::Visualization::Relator.any_instance.stubs(:user).returns(@user_mock)
@visualization = Visualization::Member.new(
{
name: 'Visualization 1',
description: 'A sample visualization',
privacy: 'public',
type: 'derived',
map_id: UUID,
id: @map_id,
user_id: @user_id
}
).store
user_fake = model_fake(@map_id, @user_id)
@subdomain = 'bogus'
@locator = Visualization::Locator.new(user_fake)
end
describe '#get' do
it 'fetches a Visualization::Member if passed an UUID' do
rehydrated = @locator.get(@visualization.id, @subdomain).first
rehydrated.name.should == @visualization.name
rehydrated.description.should_not be_nil
end
it 'fetches a Visualization::Member if passed a visualization name' do
Visualization::Collection.any_instance.stubs(:user_shared_vis).returns([])
rehydrated = @locator.get(@visualization.name, @subdomain).first
rehydrated.id.should == @visualization.id
rehydrated.description.should_not be_nil
end
it 'fetches a Table if passed a table id' do
user = create_user(quota_in_bytes: 1234567890, table_quota: 10)
user.stubs(:invalidate_varnish_cache).returns(nil)
table = Table.new
table.user_id = user.id
table.save
table.reload
table_vis = table.table_visualization
table = Visualization::Locator.new.get(table.id, user.username)
table[1].nil?.should eq false
table[1].id.should eq table_vis.table.id
user.destroy
end
it 'returns nil if no visualization or table found' do
Visualization::Collection.any_instance.stubs(:user_shared_vis).returns([])
@locator.get('220d2f46-b371-11e4-93f7-080027880ca6', @subdomain).should == [nil, nil]
end
end #get
def model_fake(map_id=nil, user_id=UUID)
model_klass = Object.new
class << model_klass
attr_accessor :id
end
model_klass.id = user_id
def model_klass.where(filter)
@called_filter = filter
[OpenStruct.new(
maps: [OpenStruct.new(id: UUID)],
id: id
)]
end
def model_klass.called_filter
@called_filter
end
model_klass
end #model_fake
end # Visualization::Locator
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,53 @@
require 'rspec/core'
require 'rspec/expectations'
require 'rspec/mocks'
require 'sequel'
require_relative '../../spec_helper'
require_relative '../../../app/models/visualization/name_checker'
include CartoDB
describe Visualization::NameChecker do
before :all do
bypass_named_maps
@user = FactoryGirl.create(:valid_user)
@user2 = FactoryGirl.create(:valid_user)
@vis1 = FactoryGirl.build(:derived_visualization, name: 'Visualization 1', user_id: @user.id).store
@vis2 = FactoryGirl.build(:derived_visualization, name: 'Visualization 2', user_id: @user.id).store
@vis3 = FactoryGirl.build(:derived_visualization, name: 'Visualization 4', user_id: @user2.id).store
@shared_entity = CartoDB::SharedEntity.new(
recipient_id: @user.id,
recipient_type: CartoDB::SharedEntity::RECIPIENT_TYPE_USER,
entity_id: @vis3.id,
entity_type: CartoDB::SharedEntity::ENTITY_TYPE_VISUALIZATION
).save
end
after :all do
@shared_entity.destroy
@vis3.destroy
@vis2.destroy
@vis1.destroy
@user.destroy
end
describe '#available?' do
it 'returns true if passed visualization name is available for the user' do
checker = Visualization::NameChecker.new(@user)
checker.available?('Visualization 3').should == true
end
it 'returns false if passed visualization name is in use by the user' do
checker = Visualization::NameChecker.new(@user)
checker.available?('Visualization 1').should == false
checker.available?('Visualization 2').should == false
end
it 'returns true if name is available but used in shared visualizations' do
checker = Visualization::NameChecker.new(@user)
checker.available?('Visualization 4').should == true
end
end # available?
end # Visualization::NameChecker
@@ -0,0 +1,83 @@
require 'ostruct'
require_relative '../../spec_helper'
require_relative '../../../app/models/visualization/name_generator'
include CartoDB
describe Visualization::NameGenerator do
describe '#generate' do
it 'returns the candidate name if available' do
user = Object.new
candidate = 'Non existing name'
checker = positive_checker
generator = Visualization::NameGenerator.new(user, checker)
generator.name(candidate).should == candidate
end
it 'returns an altered name if no available candidate' do
user = Object.new
existing = 'visualization 1'
checker = negative_checker
generator = Visualization::NameGenerator.new(user, checker)
generator.name(existing).should_not == existing
end
it 'returns a generated name if no candidate' do
user = Object.new
checker = negative_checker
generator = Visualization::NameGenerator.new(user, checker)
generator.name.should =~ /#{Visualization::NameGenerator::PATTERN}/
end
it 'accepts names with non-ASCII characters' do
user = Object.new
checker = positive_checker
candidate = 'Me gustan los ñúes'
generator = Visualization::NameGenerator.new(user, checker)
generator.name(candidate).should == candidate
end
it 'strips leading and trailing whitespace from candidates' do
user = Object.new
checker = positive_checker
candidate = ' viva la pepa '
generator = Visualization::NameGenerator.new(user, checker)
generator.name(candidate).should == 'viva la pepa'
end
it 'assigns a valid name if a explicit nil candidate is passed' do
user = Object.new
checker = positive_checker
candidate = ' viva la pepa '
generator = Visualization::NameGenerator.new(user, checker)
generator.name(nil).should == Visualization::NameGenerator::PATTERN
end
it 'assigns an incremental number' do
user = Object.new
candidate = 'Untitled map'
Visualization::NameGenerator.new(user, negative_checker([])).name(candidate).should == candidate
Visualization::NameGenerator.new(user, negative_checker([candidate])).name(candidate).should == "#{candidate} 1"
Visualization::NameGenerator.new(user, negative_checker([candidate, "#{candidate} 1"])).name(candidate).should == "#{candidate} 2"
Visualization::NameGenerator.new(user, negative_checker([candidate, "#{candidate} 1", "#{candidate} 2"])).name(candidate).should == "#{candidate} 3"
end
end
def positive_checker
checker = Object.new
def checker.available?(*args); true; end
checker
end
def negative_checker(existing = [ 'visualization 1', 'Untitled visualization', 'Untitled visualization 0', 'Untitled visualization 1'])
checker = OpenStruct.new
checker.existing = existing
def checker.available?(candidate)
!existing.include?(candidate)
end
checker
end
end
@@ -0,0 +1,196 @@
require_relative '../../spec_helper'
require_relative '../../../services/data-repository/backend/sequel'
require_relative '../../../app/models/visualization/member'
require_relative '../../../services/data-repository/repository'
require_relative '../../factories/organizations_contexts'
include CartoDB
describe Visualization::Member do
include_context 'organization with users helper'
before(:each) do
bypass_named_maps
end
describe 'sharing tables and visualizations' do
it 'should give read permission to table aka canonical visualization' do
owner_table = create_table(@org_user_owner)
carto_canonical_vis = owner_table.table_visualization
canonical_vis = CartoDB::Visualization::Member.new(id: carto_canonical_vis.id).fetch
canonical_vis.has_permission?(@org_user_owner, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq true
canonical_vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq false
give_permission(canonical_vis, @org_user_1, CartoDB::Permission::ACCESS_READONLY)
canonical_vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq true
end
it 'other user should not have permission until given' do
owner_table = create_table(@org_user_owner)
carto_vis = create_vis_from_table(owner_table.table_visualization.user, owner_table)
vis = CartoDB::Visualization::Member.new(id: carto_vis.id).fetch
vis.has_permission?(@org_user_owner, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq true
vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq false
give_permission(vis, @org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY)
vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq true
end
it 'other user will not get permission in private table when the table owners adds it to the visualization' do
owner_table = create_table(@org_user_owner)
carto_vis = create_vis_from_table(owner_table.table_visualization.user, owner_table)
vis = CartoDB::Visualization::Member.new(id: carto_vis.id).fetch
give_permission(vis, @org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY)
vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq true
other_table = create_table(@org_user_owner)
other_vis = CartoDB::Visualization::Member.new(id: other_table.table_visualization.id).fetch
add_layer_from_table(vis, other_table)
vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq true
other_vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq false
end
it 'should not remove access to visualization if table privacy is changed to private' do
owner_table = create_table(@org_user_owner)
carto_canonical_vis = owner_table.table_visualization
canonical_vis = CartoDB::Visualization::Member.new(id: carto_canonical_vis.id).fetch
carto_vis = create_vis_from_table(owner_table.table_visualization.user, owner_table)
vis = CartoDB::Visualization::Member.new(id: carto_vis.id).fetch
give_permission(vis, @org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY)
# removes access to table
canonical_vis.permission.clear
canonical_vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq false
vis.has_permission?(@org_user_1, CartoDB::Visualization::Member::PERMISSION_READONLY).should eq true
end
end
private
def add_layer_from_table(vis, table)
layer = Layer.create(layer_params(table).slice(:kind, :options, :infowindow, :tooltip, :order))
vis.map.add_layer(layer)
layer.register_table_dependencies
vis.map.process_privacy_in(layer)
end
def create_vis_from_table(user, table)
blender = Visualization::TableBlender.new(user, [table])
map = blender.blend
vis = Visualization::Member.new(
name: 'wadus_vis',
map_id: map.id,
type: Visualization::Member::TYPE_DERIVED,
privacy: blender.blended_privacy,
user_id: user.id
)
vis.store
vis
end
def give_permission(vis, user, access)
per = vis.permission
per.set_user_permission(user, access)
per.save
per.reload
end
# @return [CartoDB::Visualization::Member]
def create_table(user)
table = Table.new
table.user_id = user.id
table.name = 'wadus_table_' + UUIDTools::UUID.timestamp_create.to_s
table.save
table.reload
# table = create_table(user_id: user.id, name: 'wadus_table')
table.table_visualization.type.should eq Visualization::Member::TYPE_CANONICAL
table
end
def layer_params(table)
{
:kind => "carto",
:options => {
:attribution => 'CartoDB',
:type => 'CartoDB',
:active => true,
:query => '',
:opacity => 0.99,
:interactivity => 'cartodb_id',
:interaction => true,
:debug => false,
:tiler_domain => 'localhost.lan',
:tiler_port => '8181',
:tiler_protocol => 'http',
:sql_api_domain => 'localhost.lan',
:sql_api_port => 8080,
:sql_api_protocol => 'http',
:extra_params => {
:cache_policy => 'persist',
:cache_buster => 1404930437358
},
:maxZoom => 28,
:auto_bound => false,
:visible => true,
:sql_domain => 'localhost.lan',
:sql_port => '80',
:sql_protocol => 'http',
:tile_style_history => ["##{table.name}{ line-color: #FF6600; line-width: 2; line-opacity: 0.7; }"],
:style_version => '2.1.1',
:table_name => table.name,
:user_name => 'foo',
:tile_style => "##{table.name}{ line-color: #FF6600; line-width: 2; line-opacity: 0.7; }",
:use_server_style => true,
:query_history => [],
:wizard_properties => {
:type => 'polygon',
:properties => {
'line-width' => 2,
'line-color' => '#FF6600',
'line-opacity' => 0.7,
'line-comp-op' => 'none',
'text-name' => 'None',
'text-face-name' => 'DejaVu Sans Book',
'text-size' => 10,
'text-fill' => '#000',
'text-halo-fill' => '#FFF',
'text-halo-radius' => 1,
'text-dy' => -10,
'text-allow-overlap' => true,
'text-placement-type' => 'dummy',
'text-label-position-tolerance' => 0,
'text-placement' => 'point',
'geometry_type' => 'line'
}
},
:tile_style_custom => false,
:query_wrapper => nil,
:query_generated => false,
:order => 2,
:stat_tag => table.id,
:sql_api_endpoint => '/api/v1/sql',
:no_cdn => true,
:force_cors => true
},
:infowindow => {
:template_name => 'table/views/infowindow_light',
:fields => []
},
:tooltip => {
:fields => []
},
:order => 2
}
end
end
@@ -0,0 +1,52 @@
require 'ostruct'
require 'sequel'
require_relative '../../spec_helper'
require_relative '../../../services/data-repository/backend/sequel'
require_relative '../../../app/models/visualization/member'
require_relative '../../../app/models/visualization/overlays'
require_relative '../../../services/data-repository/repository'
include CartoDB
describe Visualization::Overlays do
before(:each) do
@db = SequelRails.connection
Visualization.repository = DataRepository::Backend::Sequel.new(@db, :visualizations)
member = Visualization::Member.new
map_mock = mock
map_mock.stubs(:scrollwheel=)
map_mock.stubs(:legends=)
map_mock.stubs(:legends)
map_mock.stubs(:scrollwheel)
map_mock.stubs(:id)
map_mock.stubs(:save)
Visualization::Member.any_instance.stubs(:map).returns(map_mock)
@visualization = member
Visualization.repository = DataRepository::Backend::Sequel.new(@db, :visualizations)
end
describe 'default' do
it 'should create all overlays' do
Visualization::Overlays.new(@visualization).create_default_overlays
@visualization.overlays.count.should eq 5
@visualization.overlays.select { |o| o.options['display'] }.count.should eq 5
@visualization.overlays.select { |o| o.type == 'logo' }.count.should eq 1
end
it 'should not create logo if user has disabled_cartodb_logo feature_flag' do
user_mock = mock
user_mock.stubs(:has_feature_flag?).with('disabled_cartodb_logo').returns(true)
@visualization.stubs(:user).returns(user_mock)
Visualization::Overlays.new(@visualization).create_default_overlays
@visualization.overlays.select { |o| o.type == 'logo' }.count.should eq 0
@visualization.overlays.count.should eq 4
end
end
end
+198
View File
@@ -0,0 +1,198 @@
require_relative '../../spec_helper'
require_relative '../../../app/models/visualization/presenter'
require_relative '../../doubles/support_tables.rb'
include CartoDB
describe Visualization::Member do
before do
@db = SequelRails.connection
Sequel.extension(:pagination)
Visualization.repository = DataRepository::Backend::Sequel.new(@db, :visualizations)
end
before(:each) do
bypass_named_maps
user_name = 'whatever'
@user_mock = FactoryGirl.build(:user, username: user_name)
CartoDB::Visualization::Relator.any_instance.stubs(:user).returns(@user_mock)
support_tables_mock = Doubles::Visualization::SupportTables.new
Visualization::Relator.any_instance.stubs(:support_tables).returns(support_tables_mock)
end
describe '#privacy_for_vizjson' do
it 'checks expected privacy values for the vizjson' do
visualization = Visualization::Member.new(
privacy: Visualization::Member::PRIVACY_PUBLIC,
name: 'test',
type: Visualization::Member::TYPE_CANONICAL
)
# Careful, do a user mock after touching user_data as it does some checks about user too
user_mock = mock
user_mock.stubs(:private_tables_enabled).returns(true)
user_mock.stubs(:id).returns(@user_mock.id)
Visualization::Member.any_instance.stubs(:user).returns(user_mock)
presenter = Visualization::Presenter.new(visualization)
presenter.send(:privacy_for_vizjson).should eq Visualization::Member::PRIVACY_PUBLIC
visualization.privacy = Visualization::Member::PRIVACY_PRIVATE
presenter.send(:privacy_for_vizjson).should eq Visualization::Member::PRIVACY_PRIVATE
visualization.privacy = Visualization::Member::PRIVACY_PROTECTED
presenter.send(:privacy_for_vizjson).should eq Visualization::Member::PRIVACY_PROTECTED
visualization.privacy = Visualization::Member::PRIVACY_LINK
presenter.send(:privacy_for_vizjson).should eq Visualization::Member::PRIVACY_PUBLIC
end
end
describe 'to_poro fields' do
it 'basic fields expected at the to_poro method' do
perm_mock = FactoryGirl.build(:carto_permission)
vis_mock = mock
vis_mock.stubs(:id).returns(UUIDTools::UUID.timestamp_create.to_s)
vis_mock.stubs(:name).returns('vis1')
vis_mock.stubs(:display_name).returns('vis1')
vis_mock.stubs(:map_id).returns(UUIDTools::UUID.timestamp_create.to_s)
vis_mock.stubs(:active_layer_id).returns(1)
vis_mock.stubs(:type).returns(Visualization::Member::TYPE_CANONICAL)
vis_mock.stubs(:tags).returns(['tag1'])
vis_mock.stubs(:description).returns('desc')
vis_mock.stubs(:privacy).returns(Visualization::Member::PRIVACY_PUBLIC)
vis_mock.stubs(:stats).returns('123')
vis_mock.stubs(:created_at).returns(Time.now)
vis_mock.stubs(:updated_at).returns(Time.now)
vis_mock.stubs(:permission).returns(perm_mock)
vis_mock.stubs(:locked).returns(true)
vis_mock.stubs(:source).returns('')
vis_mock.stubs(:license).returns('')
vis_mock.stubs(:attributions).returns('')
vis_mock.stubs(:title).returns('')
vis_mock.stubs(:parent_id).returns(nil)
vis_mock.stubs(:children).returns([])
vis_mock.stubs(:kind).returns(Visualization::Member::KIND_GEOM)
vis_mock.stubs(:table).returns(nil)
vis_mock.stubs(:related_tables).returns([])
vis_mock.stubs(:prev_id).returns(nil)
vis_mock.stubs(:next_id).returns(nil)
vis_mock.stubs(:transition_options).returns({})
vis_mock.stubs(:active_child).returns(nil)
vis_mock.stubs(:likes).returns([])
vis_mock.stubs(:synchronization).returns(nil)
presenter = Visualization::Presenter.new(vis_mock)
data = presenter.to_poro
data[:id].present?.should eq true
data[:name].present?.should eq true
data[:display_name].present?.should eq true
data[:map_id].present?.should eq true
data[:active_layer_id].present?.should eq true
data[:type].present?.should eq true
data[:tags].present?.should eq true
data[:description].present?.should eq true
data[:privacy].present?.should eq true
data[:stats].present?.should eq true
data[:created_at].present?.should eq true
data[:updated_at].present?.should eq true
data[:permission].present?.should eq true
data[:locked].present?.should eq true
data[:related_tables].should eq Array.new
data[:table].should eq Hash.new
data[:parent_id].should eq nil
data[:children].should eq Array.new
data[:kind].should eq Visualization::Member::KIND_GEOM
data[:prev_id].should eq nil
data[:next_id].should eq nil
data[:transition_options].should eq Hash.new
data[:active_child].should eq nil
end
end
describe '#children' do
it 'tests .children and its sorting' do
Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true)
::Permission.any_instance.stubs(:owner).returns(@user_mock)
parent = Visualization::Member.new(random_attributes_for_vis_member({
name:'PARENT',
user_id: @user_mock.id,
type: Visualization::Member::TYPE_DERIVED
}))
parent = parent.store.fetch
# Create unsorted on purpose
member_d = Visualization::Member.new(random_attributes_for_vis_member({
name:'D',
user_id: @user_mock.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: parent.id
}))
member_d = member_d.store.fetch
member_c = Visualization::Member.new(random_attributes_for_vis_member({
name:'C',
user_id: @user_mock.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: parent.id
}))
member_c = member_c.store.fetch
member_b = Visualization::Member.new(random_attributes_for_vis_member({
name:'B',
user_id: @user_mock.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: parent.id
}))
member_b = member_b.store.fetch
member_e = Visualization::Member.new(random_attributes_for_vis_member({
name:'E',
user_id: @user_mock.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: parent.id
}))
member_e = member_e.store.fetch
member_a = Visualization::Member.new(random_attributes_for_vis_member({
name:'A',
user_id: @user_mock.id,
type: Visualization::Member::TYPE_SLIDE,
parent_id: parent.id
}))
member_a = member_a.store.fetch
# A -> B -> C -> D -> E
member_a.set_next_list_item! member_b
member_b.set_next_list_item! member_c
member_c.set_next_list_item! member_d
member_d.set_next_list_item! member_e
member_a.fetch
member_b.fetch
member_c.fetch
member_d.fetch
member_e.fetch
parent.fetch
@request_mock = mock
@request_mock.stubs(:host).returns("#{@user_mock.username}#{CartoDB.session_domain}")
@request_mock.stubs(:fullpath).returns('')
data = Visualization::Presenter.new(parent,{request:@request_mock}).to_poro
data[:children].length.should eq 5
data[:children][0][:id].should eq member_a.id
data[:children][1][:id].should eq member_b.id
data[:children][2][:id].should eq member_c.id
data[:children][3][:id].should eq member_d.id
data[:children][4][:id].should eq member_e.id
end
end
end
+149
View File
@@ -0,0 +1,149 @@
require_relative '../../spec_helper'
require_relative '../../../app/models/visualization/relator'
require_relative '../../../app/models/visualization/member'
require_relative '../../../app/models/visualization/collection'
require_relative '../../doubles/support_tables.rb'
include CartoDB
describe Visualization::Relator do
before do
@db = SequelRails.connection
Sequel.extension(:pagination)
Visualization.repository = DataRepository::Backend::Sequel.new(@db, :visualizations)
end
before(:all) do
@user = create_user({
email: 'admin@cartotest.com',
username: 'admin',
password: '000123456'
})
@carto_user = Carto::User.find(@user.id)
end
before(:each) do
bypass_named_maps
delete_user_data(@user)
end
after(:all) do
bypass_named_maps
@user.destroy
end
describe '#estimated_row_count and #actual_row_count' do
it 'should query Table estimated an actual row count methods' do
::Table.any_instance.stubs(:row_count_and_size).returns(row_count: 999)
::Table.any_instance.stubs(:actual_row_count).returns(1000)
table = create_table(name: 'table1', user_id: @user.id)
vis = table.table_visualization
vis.estimated_row_count.should == 999
vis.actual_row_count.should == 1000
end
end
before(:each) do
# For relator->permission
@user_mock = create_mocked_user
CartoDB::Visualization::Relator.any_instance.stubs(:user).returns(@user_mock)
support_tables_mock = Doubles::Visualization::SupportTables.new
Visualization::Relator.any_instance.stubs(:support_tables).returns(support_tables_mock)
end
describe '#related_canonical_visualizations' do
it 'should return the canonical visualizations associated to a derived visualization' do
table1 = create_table({:name => 'table1', :user_id => @user.id})
table2 = create_table({:name => 'table2', :user_id => @user.id})
vis_table1 = create_vis_from_table(@carto_user, table1)
create_vis_from_table(@carto_user, table2)
vis_table1.related_canonical_visualizations.map(&:id).should == [table1.table_visualization.id]
end
end
describe '#children' do
it 'tests .children and its sorting' do
Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true)
parent = Visualization::Member.new(random_attributes_for_vis_member({
name:'PARENT',
user_id: @user_mock.id,
type: Visualization::Member::TYPE_DERIVED }))
parent = parent.store.fetch
# Create unsorted on purpose
member_d = Visualization::Member.new(random_attributes_for_vis_member({
name:'D', type: Visualization::Member::TYPE_SLIDE,
user_id: @user_mock.id,
parent_id: parent.id }))
member_d = member_d.store.fetch
member_c = Visualization::Member.new(random_attributes_for_vis_member({
name:'C', type: Visualization::Member::TYPE_SLIDE,
user_id: @user_mock.id,
parent_id: parent.id }))
member_c = member_c.store.fetch
member_b = Visualization::Member.new(random_attributes_for_vis_member({
name:'B', type: Visualization::Member::TYPE_SLIDE,
user_id: @user_mock.id,
parent_id: parent.id }))
member_b = member_b.store.fetch
member_e = Visualization::Member.new(random_attributes_for_vis_member({
name:'E', type: Visualization::Member::TYPE_SLIDE,
user_id: @user_mock.id,
parent_id: parent.id }))
member_e = member_e.store.fetch
member_a = Visualization::Member.new(random_attributes_for_vis_member({
name:'A', type: Visualization::Member::TYPE_SLIDE,
user_id: @user_mock.id,
parent_id: parent.id }))
member_a = member_a.store.fetch
# A -> B -> C -> D -> E
member_a.set_next_list_item! member_b
member_b.set_next_list_item! member_c
member_c.set_next_list_item! member_d
member_d.set_next_list_item! member_e
member_a.fetch
member_b.fetch
member_c.fetch
member_d.fetch
member_e.fetch
parent.fetch
children = parent.children
children.length.should eq 5
children[0].id.should eq member_a.id
children[1].id.should eq member_b.id
children[2].id.should eq member_c.id
children[3].id.should eq member_d.id
children[4].id.should eq member_e.id
end
end
private
def create_vis_from_table(user, table)
blender = Visualization::TableBlender.new(user, [table])
map = blender.blend
vis = Visualization::Member.new(
name: 'wadus_vis',
map_id: map.id,
type: Visualization::Member::TYPE_DERIVED,
privacy: blender.blended_privacy,
user_id: user.id
)
vis.store
vis
end
end
@@ -0,0 +1,125 @@
require_relative '../../spec_helper'
require_relative '../../../app/models/visualization/table_blender'
include CartoDB::Visualization
# TODO this file cannot be executed in isolation
describe TableBlender do
let(:user) do
FactoryGirl.create(:valid_user, private_tables_enabled: true, viewer: true)
end
let(:map_mock) do
map = mock
map.stubs(:to_hash).returns({})
map.stubs(:user).returns(user)
map.stubs(:user_layers).returns([])
map.stubs(:data_layers).returns([])
map
end
describe '#blend' do
include Carto::Factories::Visualizations
include_context 'users helper'
it 'raises an error for viewer users' do
tables = [fake_public_table, fake_private_table]
expect {
TableBlender.new(user, tables).blend
}.to raise_error(/Viewer users can't blend tables/)
user.destroy
end
describe 'multiple tables' do
it 'sets increasing order for data layers and keep tiled first and last' do
map1 = FactoryGirl.create(:carto_map_with_2_tiled_layers, user_id: @carto_user1.id)
map2 = FactoryGirl.create(:carto_map_with_2_tiled_layers, user_id: @carto_user1.id)
map1, table1, table_visualization1, visualization1 = create_full_visualization(@carto_user1, canonical_map: map1)
map2, table2, table_visualization2, visualization2 = create_full_visualization(@carto_user1, canonical_map: map2)
blender = CartoDB::Visualization::TableBlender.new(@carto_user1, [table1, table2])
map = blender.blend
map.layers.count.should eq 4
orders_and_kind = map.layers.map { |l| [l.order, l.kind] }.sort { |x, y| x[0] <=> y[0] }
orders_and_kind.should eq [[0, 'tiled'], [1, 'carto'], [2, 'carto'], [3, 'tiled']]
destroy_full_visualization(map2, table2, table_visualization2, visualization2)
destroy_full_visualization(map1, table1, table_visualization1, visualization1)
end
end
describe 'default basemap' do
before(:each) do
@map, @table, @table_visualization, @visualization = create_full_visualization(@carto_user1)
end
after(:each) do
destroy_full_visualization(@map, @table, @table_visualization, @visualization)
end
it 'is chosen from table map basemap for editor users' do
@carto_user1.builder_enabled = false
# Let's force a name change in order to be sure that it's taken from the table map and it's not a default one
fake_name = 'fake_basemap_name'
@table.map.user_layers.first.options['name'] = fake_name
@table.save
blender = CartoDB::Visualization::TableBlender.new(@carto_user1, [@table])
map = blender.blend
map.user_layers.first.options['name'].should eq fake_name
end
it 'is chosen from the default basemap for builder users' do
@carto_user1.builder_enabled = true
# Let's force a name change in order to be sure that it's not taken from the table map
fake_name = 'fake_basemap_name'
@table.map.user_layers.first.options['name'] = fake_name
@table.save
blender = CartoDB::Visualization::TableBlender.new(@carto_user1, [@table])
map = blender.blend
map.user_layers.first.options['name'].should_not eq fake_name
map.user_layers.first.options['name'].should eq Cartodb.default_basemap['name']
end
end
end
# TODO test too coupled with implementation outside blender
# refactor once Privacy is extracted
describe '#blended_privacy' do
it 'returns private if any of all tables is private' do
user = Object.new
tables = [fake_public_table, fake_private_table]
TableBlender.new(user, tables).blended_privacy.should == 'private'
tables = [fake_private_table, fake_public_table]
TableBlender.new(user, tables).blended_privacy.should == 'private'
end
it 'returns public if all tables are public' do
user = Object.new
tables = [fake_public_table, fake_public_table]
TableBlender.new(user, tables).blended_privacy.should == 'public'
end
end
def fake_public_table
table = mock
table.stubs(:private?).returns(false)
table.stubs(:public_with_link_only?).returns(false)
table.stubs(:map).returns(map_mock)
table
end
def fake_private_table
table = mock
table.stubs(:private?).returns(true)
table.stubs(:public_with_link_only?).returns(false)
table.stubs(:map).returns(map_mock)
table
end
end
+97
View File
@@ -0,0 +1,97 @@
require_relative '../../spec_helper'
require_relative '../../../services/data-repository/backend/sequel'
require_relative '../../../services/data-repository/repository'
require_relative '../../../app/models/visualization/tags'
require_relative '../../../app/models/visualization/collection'
require 'helpers/unique_names_helper'
include UniqueNamesHelper
include CartoDB
describe Visualization::Tags do
include CartoDB::Factories
before(:each) do
Varnish.any_instance.stubs(:send_command).returns(true)
@db = SequelRails.connection
Visualization.repository = DataRepository::Backend::Sequel.new(@db, :visualizations)
bypass_named_maps
# For relator->permission
user_id = UUIDTools::UUID.timestamp_create.to_s
user_name = 'whatever'
user_apikey = '123'
@user_mock = create_mocked_user(user_id: user_id, user_name: user_name, user_apikey: user_apikey)
Visualization::Relator.any_instance.stubs(:user).returns(@user_mock)
end
describe '#filters' do
it 'checks that filtering by locked works' do
vis_1_tag = 'v1tag'
vis_2_tag = 'v2tag'
Visualization::Member.new(random_attributes(user_id: @user_mock.id, name: 'viz_1', locked:true, \
tags: [vis_1_tag])).store
Visualization::Member.new(random_attributes(user_id: @user_mock.id, name: 'viz_2', locked:false, \
tags: [vis_2_tag])).store
tags = Visualization::Tags.new(@user_mock)
records = tags.names()
records.length.should eq 2
records = tags.names(locked: false)
records.length.should eq 1
records.first.should eq vis_2_tag
records = tags.names(locked: true)
records.length.should eq 1
records.first.should eq vis_1_tag
end
end
describe 'searching' do
it 'should search for tags correctly' do
manolo_tag = 'manolo'
manoloescobar_tag = 'manoloescobar'
manolo_escobar_tag = 'Manolo Escobar'
v1 = Visualization::Member.new(random_attributes(user_id: @user_mock.id, name: 'v1', locked:true, tags: [manolo_tag, manoloescobar_tag, manolo_escobar_tag])).store
v2 = Visualization::Member.new(random_attributes(user_id: @user_mock.id, name: 'v2', locked:false, tags: [manolo_tag])).store
v3 = Visualization::Member.new(random_attributes(user_id: @user_mock.id, name: 'v3', locked:false, tags: [manolo_escobar_tag, manoloescobar_tag])).store
vqb1 = Carto::VisualizationQueryBuilder.new.with_tags([manolo_tag.upcase]).build.map(&:id)
vqb1.should include v1.id
vqb1.should include v2.id # Searching for 'manolo' should bring up 'manoloescobar' as well
vqb1.should include v3.id
vqb2 = Carto::VisualizationQueryBuilder.new.with_tags([manoloescobar_tag.upcase]).build.map(&:id)
vqb2.should include v1.id
vqb2.should_not include v2.id # 'manoloescobar' is not a substring of 'manolo'
vqb2.should include v3.id
vqb3 = Carto::VisualizationQueryBuilder.new.with_tags([manolo_escobar_tag.upcase]).build.map(&:id)
vqb3.should include v1.id
vqb3.should_not include v2.id
vqb3.should include v3.id
end
end
def random_attributes(attributes={})
random = unique_name('viz')
{
name: attributes.fetch(:name, random),
description: attributes.fetch(:description, "description #{random}"),
privacy: attributes.fetch(:privacy, 'public'),
tags: attributes.fetch(:tags, ['tag 1']),
type: attributes.fetch(:type, CartoDB::Visualization::Member::TYPE_CANONICAL),
user_id: attributes.fetch(:user_id, UUIDTools::UUID.timestamp_create.to_s),
locked: attributes.fetch(:locked, false)
}
end
end
@@ -0,0 +1,36 @@
# requires implementing build_visualization
shared_examples_for 'visualization models' do
describe '#password' do
it 'checks that when using password protected type, encrypted password is generated and stored correctly' do
password_value = '000123456'
password_second_value = '456789'
visualization = build_visualization(type: Visualization::Member::TYPE_DERIVED)
visualization.privacy = Visualization::Member::PRIVACY_PROTECTED
visualization.password = password_value
visualization.has_password?.should be_true
visualization.password_valid?(password_value).should be_true
# Shouldn't remove the password, and be equal
visualization.password = ''
visualization.has_password?.should be_true
visualization.password_valid?(password_value).should be_true
visualization.password = nil
visualization.has_password?.should be_true
visualization.password_valid?(password_value).should be_true
# Modify the password
visualization.password = password_second_value
visualization.has_password?.should be_true
visualization.password_valid?(password_second_value).should be_true
visualization.password_valid?(password_value).should be_false
# Test removing the password, should work
# :remove_password doesn't need to be public, so in the new model it's kept private. :send is needed here, then.
visualization.send(:remove_password)
visualization.has_password?.should be_false
visualization.password_valid?(password_value).should be_false
end
end
end