require_relative '../../spec_helper' require_relative '../visualization_shared_examples' require_relative '../../../services/data-repository/backend/sequel' require_relative '../../../app/models/visualization/member' require_relative '../../../app/models/visualization/collection' require_relative '../../../services/data-repository/repository' require_relative '../../doubles/support_tables.rb' require_dependency 'cartodb/redis_vizjson_cache' include CartoDB describe Visualization::Member do before(:all) do @db = SequelRails.connection Sequel.extension(:pagination) Visualization.repository = DataRepository::Backend::Sequel.new(@db, :visualizations) @user = FactoryGirl.create(:valid_user) end after(:all) do @user.destroy end before(:each) do bypass_named_maps # For relator->permission user_id = Carto::UUIDHelper.random_uuid user_name = 'whatever' user_apikey = '123' @user_mock = mock @user_mock.stubs(:id).returns(user_id) @user_mock.stubs(:username).returns(user_name) @user_mock.stubs(:api_key).returns(user_apikey) @user_mock.stubs(:viewer).returns(false) @user_mock.stubs(:has_feature_flag?).returns(false) @user_mock.stubs(:new_visualizations_version).returns(2) 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 it_behaves_like 'visualization models' do def build_visualization(attrs = {}) Visualization::Member.new(attrs) end end describe '#initialize' do it 'assigns an id by default' do member = Visualization::Member.new member.should be_an_instance_of Visualization::Member member.id.should_not be_nil end end describe '#store' do it 'should fail if no user_id attribute present' do attributes = random_attributes_for_vis_member(user_id: @user_mock.id) attributes.delete(:user_id) member = Visualization::Member.new(attributes) expect { member.store }.to raise_exception CartoDB::InvalidMember end it 'should fail for new visualizations and viewer users' do @user_mock.stubs(:viewer).returns(true) attributes = random_attributes_for_vis_member(user_id: @user_mock.id) member = Visualization::Member.new(attributes) expect { member.store }.to raise_error(CartoDB::InvalidMember, /Viewer users can't store visualizations/) @user_mock.stubs(:viewer).returns(false) end it 'should fail for existing visualizations if user is now a viewer' do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store member = Visualization::Member.new(id: member.id).fetch @user_mock.stubs(:viewer).returns(true) member.name = 'changed' expect { member.store }.to raise_error(CartoDB::InvalidMember, /Viewer users can't store visualizations/) @user_mock.stubs(:viewer).returns(false) end it 'persists attributes to the data repository' do attributes = random_attributes_for_vis_member(user_id: @user_mock.id) member = Visualization::Member.new(attributes) member.store member = Visualization::Member.new(id: member.id) member.name.should be_nil member.fetch member.name .should == attributes.fetch(:name) member.active_layer_id .should == attributes.fetch(:active_layer_id) member.privacy .should == attributes.fetch(:privacy) member.permission.should_not be nil member.permission.owner_id.should eq @user_mock.id member.permission.owner_username.should eq @user_mock.username end it 'persists tags as an array if the backend supports it' do Permission.any_instance.stubs(:update_shared_entities).returns(nil) attributes = random_attributes_for_vis_member(user_id: @user_mock.id, tags: ['tag 1', 'tag 2']) member = Visualization::Member.new(attributes) member.store member = Visualization::Member.new(id: member.id) member.fetch member.tags.should include('tag 1') member.tags.should include('tag 2') member.delete end it 'persists tags as JSON if the backend does not support arrays' do attributes = random_attributes_for_vis_member(user_id: @user_mock.id, tags: ['tag 1', 'tag 2']) member = Visualization::Member.new(attributes) member.store member = Visualization::Member.new(id: member.id) member.fetch member.tags.should include('tag 1') member.tags.should include('tag 2') end it 'prevents empty tags from being created' do attributes = random_attributes_for_vis_member(user_id: @user_mock.id, tags: ['tag 1', '', ' ']) member = Visualization::Member.new(attributes) member.store member = Visualization::Member.new(id: member.id) member.fetch member.tags.should eq ['tag 1'] end it 'invalidates vizjson cache in varnish if name changed' do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store CartoDB::Visualization::NameChecker.any_instance.stubs(:available?).returns(true) member = Visualization::Member.new(id: member.id).fetch CartoDB::Varnish.any_instance.expects(:purge).with(member.varnish_vizjson_key) member.name = 'changed' member.store end it 'invalidates vizjson cache in varnish if privacy changed' do # Need to at least have this decorated in the user data or checks before becoming private will raise an error CartoDB::Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store member = Visualization::Member.new(id: member.id).fetch CartoDB::Varnish.any_instance.expects(:purge).with(member.varnish_vizjson_key) member.privacy = Visualization::Member::PRIVACY_PRIVATE member.store end it 'invalidates vizjson cache in varnish if description changed' do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store member = Visualization::Member.new(id: member.id).fetch CartoDB::Varnish.any_instance.expects(:purge).with(member.varnish_vizjson_key) member.description = 'changed description' member.store end end describe '#fetch' do it 'fetches attributes from the data repository' do attributes = random_attributes_for_vis_member(user_id: @user_mock.id) member = Visualization::Member.new(attributes).store member = Visualization::Member.new(id: member.id) member.name = 'changed' member.fetch member.name.should == attributes.fetch(:name) end end describe '#delete' do it 'fails for viewer users' do CartoDB::Visualization::Relator.any_instance.stubs(:children).returns([]) member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)).store member.fetch @user_mock.stubs(:viewer).returns(true) expect { member.delete }.to raise_error(CartoDB::InvalidMember, /Viewer users can't delete visualizations/) @user_mock.stubs(:viewer).returns(false) end it 'deletes this member data from the data repository' do CartoDB::Visualization::Relator.any_instance.stubs(:children).returns([]) member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)).store member.fetch member.name.should_not be_nil member.delete member.name.should be_nil lambda { member.fetch }.should raise_error KeyError end it 'invalidates vizjson cache' do CartoDB::Visualization::Relator.any_instance.stubs(:children).returns([]) member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store member.expects(:invalidate_cache) member.delete end describe 'dataset' do include Carto::Factories::Visualizations before(:all) do @user = FactoryGirl.create(:carto_user) @other_table = FactoryGirl.create(:carto_user_table, user: @user) end before(:each) do @map, @table, @table_visualization, @visualization = create_full_visualization(@user) @visualization.data_layers.first.user_tables << @table end after(:each) do destroy_full_visualization(@map, @table, @table_visualization, @visualization) end after(:all) do @user.destroy end it 'destroys maps if they are dependent' do table_visualization = CartoDB::Visualization::Member.new(id: @table_visualization.id).fetch table_visualization.delete Carto::Visualization.exists?(@visualization.id).should be_false end it 'destroys maps with join analyses if they are dependent' do # First layer uses tables @table, Second layer uses tables @table and @other_table. Map is dependent on @table layer = FactoryGirl.build(:carto_layer, kind: 'carto', maps: [@map]) layer.options[:query] = "SELECT * FROM #{@other_table.name}" layer.save layer.user_tables << @table << @other_table table_visualization = CartoDB::Visualization::Member.new(id: @table_visualization.id).fetch table_visualization.delete Carto::Visualization.exists?(@visualization.id).should be_false end it 'unlinks only dependent data layers' do layer_to_be_deleted = @visualization.data_layers.first layer = FactoryGirl.build(:carto_layer, kind: 'carto', maps: [@map]) layer.options[:query] = "SELECT * FROM #{@other_table.name}" layer.save layer.user_tables << @other_table table_visualization = CartoDB::Visualization::Member.new(id: @table_visualization.id).fetch table_visualization.delete Carto::Visualization.exists?(@visualization.id).should be_true Carto::Layer.exists?(layer_to_be_deleted.id).should be_false Carto::Layer.exists?(layer.id).should be_true end it 'deletes related analysis' do layer_to_be_deleted = @visualization.data_layers.first layer_to_be_deleted.options[:source] = 'a0' layer_to_be_deleted.save layer_to_be_deleted.user_tables << @table layer = FactoryGirl.build(:carto_layer, kind: 'carto', maps: [@map]) layer.options[:source] = 'b0' layer.save layer.user_tables << @other_table # We are doing dependencies manually because the physical table does not exist Carto::Map.any_instance.stubs(:update_dataset_dependencies) analysis_to_be_deleted = @visualization.analyses.create(user: @user, analysis_definition: { id: 'a0' }) analysis_to_keep = @visualization.analyses.create(user: @user, analysis_definition: { id: 'b0' }) table_visualization = CartoDB::Visualization::Member.new(id: @table_visualization.id).fetch table_visualization.delete Carto::Visualization.exists?(@visualization.id).should be_true Carto::Layer.exists?(layer_to_be_deleted.id).should be_false Carto::Layer.exists?(layer.id).should be_true Carto::Analysis.exists?(analysis_to_be_deleted.id).should be_false Carto::Analysis.exists?(analysis_to_keep.id).should be_true end it 'does not delete any metadata in case of deletion error' do canonical_map = @table_visualization.map canonical_layer = @table_visualization.data_layers.first Table.any_instance.stubs(:remove_table_from_user_database).raises(Sequel::DatabaseError.new('cannot drop')) table_visualization = CartoDB::Visualization::Member.new(id: @table_visualization.id).fetch expect { table_visualization.delete }.to raise_error Carto::Visualization.exists?(@visualization.id).should be_true Carto::Visualization.exists?(@table_visualization.id).should be_true Carto::Layer.exists?(canonical_layer.id).should be_true Carto::UserTable.exists?(@table.id).should be_true Carto::Map.exists?(canonical_map.id).should be_true Table.any_instance.unstub(:remove_table_from_user_database) end end end describe '#unlink_from' do it 'invalidates varnish cache' do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)).store member.expects(:invalidate_cache) member.expects(:remove_layers_from) member.unlink_from(Object.new) end end describe '#public?' do it 'returns true if privacy set to public' do visualization = Visualization::Member.new(privacy: 'public') visualization.public?.should == true visualization.privacy = Visualization::Member::PRIVACY_PRIVATE visualization.public?.should == false visualization.privacy = Visualization::Member::PRIVACY_PUBLIC visualization.public?.should == true end end describe '#permissions' do it 'checks is_owner? permissions' do user_id = Carto::UUIDHelper.random_uuid member = Visualization::Member.new(name: 'foo', user_id: user_id) user = OpenStruct.new(id: user_id) member.is_owner?(user).should == true user = OpenStruct.new(id: Carto::UUIDHelper.random_uuid) member.is_owner?(user).should == false end def mock_user(username, viewer: false) user_mock = mock user_mock.stubs(:id).returns(Carto::UUIDHelper.random_uuid) user_mock.stubs(:username).returns(username) user_mock.stubs(:organization).returns(nil) user_mock.stubs(:viewer).returns(viewer) user_mock end it 'checks has_permission? permissions' do Permission.any_instance.stubs(:grant_db_permission).returns(nil) Permission.any_instance.stubs(:notify_permissions_change).returns(nil) user2_mock = mock_user('user2') user3_mock = mock_user('user3') user4_mock = mock_user('user4') viewer_user = mock_user('user_v', viewer: true) visualization = Visualization::Member.new( privacy: Visualization::Member::PRIVACY_PUBLIC, name: 'test', type: Visualization::Member::TYPE_CANONICAL, user_id: @user.id ) visualization.store permission = Permission.where(id: visualization.permission_id).first acl = [ { type: Permission::TYPE_USER, entity: { id: user2_mock.id, username: user2_mock.username }, access: Permission::ACCESS_READONLY }, { type: Permission::TYPE_USER, entity: { id: user3_mock.id, username: user3_mock.username }, access: Permission::ACCESS_READWRITE }, { type: Permission::TYPE_USER, entity: { id: viewer_user.id, username: viewer_user.username }, access: Permission::ACCESS_READWRITE } ] acl_expected = [ { type: Permission::TYPE_USER, id: user2_mock.id, access: Permission::ACCESS_READONLY }, { type: Permission::TYPE_USER, id: user3_mock.id, access: Permission::ACCESS_READWRITE }, { type: Permission::TYPE_USER, id: viewer_user.id, access: Permission::ACCESS_READWRITE } ] permission.acl = acl permission.save visualization.permission.acl.should eq acl_expected visualization.has_permission?(user2_mock, Visualization::Member::PERMISSION_READONLY).should eq true visualization.has_permission?(user2_mock, Visualization::Member::PERMISSION_READWRITE).should eq false visualization.has_permission?(user3_mock, Visualization::Member::PERMISSION_READONLY).should eq true visualization.has_permission?(user3_mock, Visualization::Member::PERMISSION_READWRITE).should eq true visualization.has_permission?(user4_mock, Visualization::Member::PERMISSION_READONLY).should eq false visualization.has_permission?(user4_mock, Visualization::Member::PERMISSION_READWRITE).should eq false # Viewer users hasn't RW permission even though granted visualization.has_permission?(viewer_user, Visualization::Member::PERMISSION_READWRITE).should eq false delete_user_data(@user) end end describe 'validations' do describe '#privacy' do it 'must be present' do visualization = Visualization::Member.new visualization.valid?.should == false visualization.errors.fetch(:privacy).should_not be_nil end it 'must be valid' do visualization = Visualization::Member.new(privacy: 'wadus') visualization.valid?.should == false visualization.errors.fetch(:privacy).should_not be_nil end end describe '#name' do it 'downcases names for table_visualizations' do visualization = Visualization::Member.new(type: 'table') visualization.name = 'visualization_1' visualization.name.should == 'visualization_1' visualization.name = 'Visualization_1' visualization.name.should == 'visualization_1' visualization = Visualization::Member.new(type: 'derived') visualization.name = 'Visualization 1' visualization.name.should == 'Visualization 1' end end describe '#full_errors' do it 'returns full error messages' do visualization = Visualization::Member.new visualization.valid?.should == false visualization.full_errors.join("\n").should =~ /privacy/ visualization.full_errors.join("\n").should =~ /type/ visualization.full_errors.join("\n").should =~ /name/ end end end describe '#derived?' do it 'returns true if type is derived' do visualization = Visualization::Member.new(type: Visualization::Member::TYPE_DERIVED) visualization.derived?.should be_true visualization.table?.should be_false visualization.type_slide?.should be_false visualization.type = 'bogus' visualization.derived?.should be_false visualization.table?.should be_false visualization.type_slide?.should be_false end end describe '#table?' do it "returns true if type is 'table'" do visualization = Visualization::Member.new(type: Visualization::Member::TYPE_CANONICAL) visualization.derived?.should be_false visualization.table?.should be_true visualization.type_slide?.should be_false visualization.type = 'bogus' visualization.derived?.should be_false visualization.table?.should be_false visualization.type_slide?.should be_false end end describe '#type_slide?' do it "returns true if type is 'slide'" do visualization = Visualization::Member.new(type: Visualization::Member::TYPE_SLIDE) visualization.derived?.should be_false visualization.table?.should be_false visualization.type_slide?.should be_true visualization.type = 'bogus' visualization.derived?.should be_false visualization.table?.should be_false visualization.type_slide?.should be_false end end describe '#privacy_and_exceptions' do it 'checks different privacy options to make sure exceptions are raised when they should' do user_id = Carto::UUIDHelper.random_uuid visualization = Visualization::Member.new(type: Visualization::Member::TYPE_DERIVED) visualization.name = 'test' visualization.user_id = user_id @user_mock.stubs(:invalidate_varnish_cache) # Private maps allowed @user_mock.stubs(:private_maps_enabled?).returns(true) # Forces internal "dirty" flag visualization.privacy = Visualization::Member::PRIVACY_PUBLIC visualization.privacy = Visualization::Member::PRIVACY_PRIVATE visualization.store # ------------- # No private maps allowed @user_mock.stubs(:private_maps_enabled?).returns(false) visualization.privacy = Visualization::Member::PRIVACY_PUBLIC visualization.privacy = Visualization::Member::PRIVACY_PRIVATE # Derived table without private maps flag = error expect { visualization.store }.to raise_exception CartoDB::InvalidMember # ------------- visualization = Visualization::Member.new(type: Visualization::Member::TYPE_CANONICAL) visualization.name = 'test' visualization.user_id = user_id # No private maps allowed yet visualization.privacy = Visualization::Member::PRIVACY_PUBLIC visualization.privacy = Visualization::Member::PRIVACY_PRIVATE # Canonical visualizations can always be private visualization.store end end describe '#validation_for_link_privacy' do it 'checks that only users with private tables enabled can set LINK privacy' do user_id = Carto::UUIDHelper.random_uuid Visualization::Member.any_instance.stubs(:named_maps) visualization = Visualization::Member.new( privacy: Visualization::Member::PRIVACY_PUBLIC, name: 'test', type: Visualization::Member::TYPE_CANONICAL, user_id: user_id ) @user_mock.stubs(:private_maps_enabled?).returns(true) # Careful, do a user mock after touching user_data as it does some checks about user too user_mock = mock user_mock.stubs(:viewer).returns(false) user_mock.stubs(:private_tables_enabled).returns(true) user_mock.stubs(:id).returns(user_id) Visualization::Member.any_instance.stubs(:user).returns(user_mock) visualization.valid?.should eq true visualization.privacy = Visualization::Member::PRIVACY_PRIVATE visualization.valid?.should eq true visualization.privacy = Visualization::Member::PRIVACY_LINK visualization.valid?.should eq true visualization.privacy = Visualization::Member::PRIVACY_PUBLIC user_mock.stubs(:private_tables_enabled).returns(false) visualization.valid?.should eq true visualization.privacy = Visualization::Member::PRIVACY_LINK visualization.valid?.should eq false # "Reset" visualization = Visualization::Member.new( privacy: Visualization::Member::PRIVACY_LINK, name: 'test', type: Visualization::Member::TYPE_CANONICAL, user_id: user_id ) # Unchanged visualizations could be visualization.valid?.should eq true # Simulate editing the privacy visualization.stubs(:privacy_changed).returns(true) # Now it can't visualization.valid?.should eq false end end describe '#default_privacy_values' do it 'Checks deault privacies for visualizations' do user_id = Carto::UUIDHelper.random_uuid user_mock = mock user_mock.stubs(:id).returns(user_id) # We don't care about values, just want an instance visualization = Visualization::Member.new( privacy: Visualization::Member::PRIVACY_PUBLIC, name: 'test', type: Visualization::Member::TYPE_CANONICAL, user_id: user_id ) user_mock.stubs(:private_tables_enabled).returns(true) visualization.stubs(:user).returns(user_mock) visualization.default_privacy.should eq Visualization::Member::PRIVACY_LINK user_mock.stubs(:private_tables_enabled).returns(false) visualization.stubs(:user).returns(user_mock) visualization.default_privacy.should eq Visualization::Member::PRIVACY_PUBLIC end end it 'should not allow to change permission from the outside' do member = Visualization::Member.new(random_attributes_for_vis_member({user_id: @user.id})) member.store member = Visualization::Member.new(id: member.id).fetch member.permission.should_not be nil member.permission_id = Carto::UUIDHelper.random_uuid member.valid?.should eq false delete_user_data(@user) end it 'checks that slides can have a parent_id' do Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) expected_errors = { parent_id: 'Type slide must have a parent' } member = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, type: Visualization::Member::TYPE_DERIVED })) member.store member = Visualization::Member.new(id: member.id).fetch member.parent_id.should be nil member.parent.should be nil child_member = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, type: Visualization::Member::TYPE_SLIDE })) # Can't save a children of type slide without parent_id expect { child_member.store }.to raise_exception InvalidMember child_member.valid?.should eq false child_member.errors.should eq expected_errors child_member = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, type: Visualization::Member::TYPE_SLIDE, parent_id: member.id })) child_member.store child_member = Visualization::Member.new(id: child_member.id).fetch child_member.type.should eq Visualization::Member::TYPE_SLIDE child_member.parent_id.should eq member.id child_member.parent.id.should eq member.id expect { table_member = Visualization::Member.new( random_attributes_for_vis_member({ user_id: @user_mock.id, type: Visualization::Member::TYPE_CANONICAL, parent_id: member.id })) table_member.store }.to raise_exception CartoDB::InvalidMember expect { table_member = Visualization::Member.new( random_attributes_for_vis_member({ user_id: @user_mock.id, type: Visualization::Member::TYPE_DERIVED, parent_id: member.id })) table_member.store }.to raise_exception CartoDB::InvalidMember child_member = Visualization::Member.new( random_attributes_for_vis_member({ user_id: @user_mock.id, type: Visualization::Member::TYPE_SLIDE, parent_id: Carto::UUIDHelper.random_uuid })) expect { child_member.store }.to raise_exception CartoDB::InvalidMember member_2 = Visualization::Member.new( random_attributes_for_vis_member({ user_id: @user_mock.id, type: Visualization::Member::TYPE_CANONICAL })) member_2.store.fetch child_member.parent_id = member_2.id expect { child_member.store }.to raise_exception InvalidMember end it 'tests transition_options field jsonification' do Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) transition_options = { first: true, second: 6 } member = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, transition_options: transition_options })) member.transition_options.should eq transition_options member.slide_transition_options.should eq ::JSON.dump(transition_options) transition_options[:third] = 'testing' member.transition_options = transition_options member.transition_options.should eq transition_options member.slide_transition_options.should eq ::JSON.dump(transition_options) member = member.store.fetch member.transition_options.should eq transition_options member.slide_transition_options.should eq ::JSON.dump(transition_options) end describe '#linked_list_tests' do it 'checks set_next! and unlink_self_from_list! on visualizations when set' do Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) member_a = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'A', type: Visualization::Member::TYPE_DERIVED })) member_a = member_a.store.fetch member_b = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'B', type: Visualization::Member::TYPE_DERIVED })) member_b = member_b.store.fetch member_c = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'C', type: Visualization::Member::TYPE_DERIVED })) member_c = member_c.store.fetch member_d = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'D', type: Visualization::Member::TYPE_DERIVED })) member_d = member_d.store.fetch # A member_a.prev_id.should eq nil member_a.prev_list_item.should eq nil member_a.next_id.should eq nil member_a.next_list_item.should eq nil # A -> B member_a.set_next_list_item! member_b member_a.next_list_item.should eq member_b member_a.prev_list_item.should eq nil member_b.prev_list_item.should eq member_a member_b.next_list_item.should eq nil # A -> B -> C member_b.set_next_list_item! member_c # set_next reloads "actor and subject", but other affected items need to be reloaded member_a.fetch member_a.prev_list_item.should eq nil member_a.next_list_item.should eq member_b member_b.prev_list_item.should eq member_a member_b.next_list_item.should eq member_c member_c.prev_list_item.should eq member_b member_c.next_list_item.should eq nil # A -> D -> B -> C member_a.set_next_list_item! member_d member_b.fetch member_c.fetch member_a.prev_list_item.should eq nil member_a.next_list_item.should eq member_d member_d.prev_list_item.should eq member_a member_d.next_list_item.should eq member_b member_b.prev_list_item.should eq member_d member_b.next_list_item.should eq member_c member_c.prev_list_item.should eq member_b member_c.next_list_item.should eq nil member_a.next_list_item.next_list_item.should eq member_b member_a.next_list_item.next_list_item.next_list_item.should eq member_c member_a.next_list_item.next_list_item.next_list_item.next_list_item.should eq nil # A -> D -> C member_b.delete # triggers unlink_self_from_list! inside, should reorder remaining items member_c.fetch member_d.fetch member_a.prev_list_item.should eq nil member_a.next_list_item.should eq member_d member_d.prev_list_item.should eq member_a member_d.next_list_item.should eq member_c member_c.prev_list_item.should eq member_d member_c.next_list_item.should eq nil # D -> C member_a.delete member_d.fetch member_d.prev_list_item.should eq nil member_d.next_list_item.should eq member_c member_c.prev_list_item.should eq member_d member_c.next_list_item.should eq nil # D member_c.delete member_d.fetch member_d.prev_list_item.should eq nil member_d.next_list_item.should eq nil member_d.delete end it 'checks reordering visualizations items' do Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) member_a = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'A', type: Visualization::Member::TYPE_DERIVED })) member_a = member_a.store.fetch member_b = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'B', type: Visualization::Member::TYPE_DERIVED })) member_b = member_b.store.fetch member_c = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'C', type: Visualization::Member::TYPE_DERIVED })) member_c = member_c.store.fetch member_d = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'D', type: Visualization::Member::TYPE_DERIVED })) member_d = member_d.store.fetch member_e = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'E', type: Visualization::Member::TYPE_DERIVED })) member_e = member_e.store.fetch # A -> B member_a.set_next_list_item! member_b # A -> B -> C member_b.set_next_list_item! member_c member_a.fetch # A -> B -> C -> D member_c.set_next_list_item! member_d member_a.fetch member_b.fetch # A -> B -> C -> D -> E member_d.set_next_list_item! member_e member_a.fetch member_b.fetch member_c.fetch member_a.prev_list_item.should eq nil member_a.next_list_item.should eq member_b member_b.prev_list_item.should eq member_a member_b.next_list_item.should eq member_c member_c.prev_list_item.should eq member_b member_c.next_list_item.should eq member_d member_d.prev_list_item.should eq member_c member_d.next_list_item.should eq member_e member_e.prev_list_item.should eq member_d member_e.next_list_item.should eq nil # Actual reordering starts here # ----------------------------- # B -> A -> C -> D -> E member_b.set_next_list_item! member_a member_c.fetch member_b.prev_list_item.should eq nil member_b.next_list_item.should eq member_a member_a.prev_list_item.should eq member_b member_a.next_list_item.should eq member_c member_c.prev_list_item.should eq member_a member_c.next_list_item.should eq member_d member_d.prev_list_item.should eq member_c member_d.next_list_item.should eq member_e member_e.prev_list_item.should eq member_d member_e.next_list_item.should eq nil # B -> A -> D -> E -> C member_e.set_next_list_item! member_c member_a.fetch member_d.fetch member_b.prev_list_item.should eq nil member_b.next_list_item.should eq member_a member_a.prev_list_item.should eq member_b member_a.next_list_item.should eq member_d member_d.prev_list_item.should eq member_a member_d.next_list_item.should eq member_e member_e.prev_list_item.should eq member_d member_e.next_list_item.should eq member_c member_c.prev_list_item.should eq member_e member_c.next_list_item.should eq nil # B -> E -> A -> D -> C member_b.set_next_list_item! member_e member_a.fetch member_c.fetch member_d.fetch member_b.prev_list_item.should eq nil member_b.next_list_item.should eq member_e member_e.prev_list_item.should eq member_b member_e.next_list_item.should eq member_a member_a.prev_list_item.should eq member_e member_a.next_list_item.should eq member_d member_d.prev_list_item.should eq member_a member_d.next_list_item.should eq member_c member_c.prev_list_item.should eq member_d member_c.next_list_item.should eq nil # Cleanup member_a.delete member_b.fetch member_b.delete member_c.fetch member_c.delete member_d.fetch member_d.delete member_e.fetch member_e.delete end it 'checks that upon destruction children are destroyed too' do Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) support_tables_mock = Doubles::Visualization::SupportTables.new Visualization::Relator.any_instance.stubs(:support_tables).returns(support_tables_mock) starting_collection_count = Visualization::Collection.new.fetch.count parent = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name: 'PARENT', type: Visualization::Member::TYPE_DERIVED })).store child1 = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name: 'CHILD 1', type: Visualization::Member::TYPE_SLIDE, parent_id: parent.id })).store.fetch child2 = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name: 'CHILD 2', type: Visualization::Member::TYPE_SLIDE, parent_id: parent.id })).store.fetch child2.set_prev_list_item!(child1) parent.fetch parent.delete Visualization::Collection.new.fetch.count.should eq starting_collection_count end it 'checks transactional wrappings for prev-next' do Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) member_a = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'A', type: Visualization::Member::TYPE_DERIVED })) member_a = member_a.store.fetch member_b = Visualization::Member.new(random_attributes_for_vis_member({ user_id: @user_mock.id, name:'B', type: Visualization::Member::TYPE_DERIVED })) member_b = member_b.store.fetch # trick to not stub but also check that validator and other internal fields are reset upon fetch, etc. member_b.privacy = 'invalid value' expect { member_a.set_next_list_item! member_b }.to raise_error member_a.fetch member_b.fetch member_a.next_id.should eq nil member_b.prev_id.should eq nil member_b.privacy.should eq Visualization::Member::PRIVACY_PUBLIC member_b.privacy = 'invalid value' expect { member_b.set_prev_list_item! member_a }.to raise_error member_a.fetch member_b.fetch member_a.next_id.should eq nil member_b.prev_id.should eq nil member_b.privacy.should eq Visualization::Member::PRIVACY_PUBLIC member_a.set_next_list_item! member_b member_a.privacy = 'invalid value' CartoDB::Visualization::Relator.any_instance.stubs(:next_list_item) do raise "Forced error" end expect { member_a.unlink_self_from_list! }.to raise_error member_a = member_a.fetch member_b = member_b.fetch member_a.next_id.should eq member_b.id member_b.prev_id.should eq member_a.id end end describe '#to_vizjson' do it "calculates and returns the vizjson if not cached" do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store mocked_vizjson = {mocked: 'vizjson'} member.expects(:calculate_vizjson).returns(mocked_vizjson).once member.to_vizjson.should eq mocked_vizjson end it "Returns the vizjson if it was cached before" do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store mocked_vizjson = {mocked: 'vizjson'} member.expects(:calculate_vizjson).returns(mocked_vizjson).once vizjson = member.to_vizjson # calculate and cache member.to_vizjson.should eq vizjson end end describe '#redis_cached' do it "Uses the block given to calculate a hash if there's a cache miss" do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) redis_cache = mock redis_cache.expects(:get).returns(nil).once # cache miss redis_cache.expects(:setex).once Visualization::RedisVizjsonCache.any_instance.stubs(:redis).returns(redis_cache) any_hash = { any_key: 'any_value' } member.expects(:calculate_vizjson).once.returns(any_hash) member.to_vizjson.should eq any_hash end it "Caches an arbitrary hash serialized if there's a miss " do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) any_hash = { any_key: 'any_value' } key = 'any_key' Visualization::RedisVizjsonCache.any_instance.stubs(:key).returns(key) redis_cache = mock redis_cache.expects(:get).returns(nil).once # cache miss redis_cache.expects(:setex).once.with(key, 24.hours.to_i, any_hash.to_json) Visualization::RedisVizjsonCache.any_instance.stubs(:redis).returns(redis_cache) member.expects(:calculate_vizjson).once.returns(any_hash) member.to_vizjson.should eq any_hash end it "Deserializes an arbitrary hash previously stored if there's a cache hit" do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) any_hash = { any_key: 'any_value' } any_hash_serialized = any_hash.to_json key = 'any_key' redis_cache = mock redis_cache.expects(:get).returns(any_hash_serialized).once # cache hit redis_cache.expects(:setex).never Visualization::RedisVizjsonCache.any_instance.stubs(:redis).returns(redis_cache) member.expects(:calculate_vizjson).never member.to_vizjson.should eq any_hash end it "Does not execute the block if there's a cache hit" do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) any_hash_serialized = {}.to_json key = 'any_key' redis_cache = mock redis_cache.expects(:get).once.returns(any_hash_serialized) # cache hit redis_cache.expects(:setex).never Visualization::RedisVizjsonCache.any_instance.stubs(:redis).returns(redis_cache) member.expects(:calculate_vizjson).never member.to_vizjson end end describe '#invalidate_cache' do it "Invalidates the varnish and redis caches" do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.expects(:invalidate_varnish_vizjson_cache).once member.expects(:invalidate_redis_cache).once member.invalidate_cache end end describe '#invalidate_redis_cache' do it "Invalidates the vizjson in redis cache" do member = Visualization::Member.new(random_attributes_for_vis_member(user_id: @user_mock.id)) member.store mocked_vizjson = {mocked: 'vizjson'} member.expects(:calculate_vizjson).returns(mocked_vizjson).once vizjson = member.to_vizjson redis_vizjson_cache = Visualization::RedisVizjsonCache.new($tables_metadata) redis_key = redis_vizjson_cache.key(member.id) redis_vizjson_cache.send(:redis).get(redis_key).should eq vizjson.to_json member.invalidate_cache redis_vizjson_cache.send(:redis).get(redis_key).should be_nil end end describe 'licenses' do before(:all) do @user = create_user end after(:all) do @user.delete end it 'should store correctly a visualization with its license' do table = create_table({:name => 'table1', :user_id => @user.id}) vis = CartoDB::Visualization::Member.new(id: table.table_visualization.id).fetch vis.license = "apache" vis.store vis.fetch vis.license_info.id.should eq :apache vis.license_info.name.should eq "Apache license" end it 'should return nil if the license is nil, empty or unkown' do table = create_table({:name => 'table1', :user_id => @user.id}) vis = CartoDB::Visualization::Member.new(id: table.table_visualization.id).fetch vis.license = nil vis.store vis.fetch vis.license_info.nil?.should eq true vis.license = "" vis.store vis.fetch vis.license_info.nil?.should eq true # I cant save with a wrong value vis.stubs(:license).returns("lololo") vis.license_info.nil?.should eq true end it 'should raise exception when try to store a unknown license, empty or nil' do table = create_table({:name => 'table1', :user_id => @user.id}) vis = CartoDB::Visualization::Member.new(id: table.table_visualization.id).fetch vis.license = "wadus" expect { vis.store }.to raise_error CartoDB::InvalidMember vis.license = "" expect { vis.store }.to raise_error CartoDB::InvalidMember vis.license = nil expect { vis.store }.to raise_error CartoDB::InvalidMember end end describe 'remote member' do before(:all) do @user = create_user @name = "example_name" @display_name = "Example display name" @user_id = @user.id @privacy = "public" @description = "Example description" @tags = ["tag1", "tag2"] @license = "apache" @source = "[source](http://www.example.com)" @attributions = "Attributions example" end it 'should create a new remote member' do remote_member = CartoDB::Visualization::Member.remote_member( @name, @user_id, @privacy, @description, @tags, @license, @source, @attributions, @display_name ) remote_member.name.should eq @name remote_member.user_id.should eq @user_id remote_member.privacy.should eq @privacy remote_member.description.should eq @description remote_member.tags.should eq @tags remote_member.license.should eq @license remote_member.source.should eq @source remote_member.attributions.should eq @attributions remote_member.display_name.should eq @display_name end end end