Initial commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: '')
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user