From 4d95cc815fbefcbd80f1eae7244ff5240e2708b4 Mon Sep 17 00:00:00 2001 From: ulferts Date: Tue, 25 Jul 2017 08:46:51 +0200 Subject: [PATCH] Feature/guarded relation columns (#5784) * refactor case statement * guard relation columns by enterprise token [ci skip] --- .../work_packages/columns/relation_column.rb | 39 +++ .../columns/relation_of_type_column.rb | 6 +- .../columns/relation_to_type_column.rb | 8 +- .../authorization/enterprise_service.rb | 18 +- .../multi_value_custom_field_spec.rb | 5 +- spec/features/support/work_package_table.rb | 27 -- .../work_packages/table/relations_spec.rb | 54 ++-- .../work_packages/table_sorting_spec.rb | 3 +- spec/models/enterprise_token_spec.rb | 10 +- .../columns/relation_of_type_column_spec.rb | 31 ++- .../columns/relation_to_type_column_spec.rb | 50 +++- spec/models/query_spec.rb | 86 +++++-- .../api/v3/queries/create_form_api_spec.rb | 235 ++++++++++++------ .../api/v3/queries/update_form_api_spec.rb | 233 +++++++++++------ .../authorization/enterprise_service_spec.rb | 17 +- .../components/work_packages/columns.rb | 110 ++++++++ spec/views/layouts/base.html.erb_spec.rb | 1 + 17 files changed, 671 insertions(+), 262 deletions(-) create mode 100644 app/models/queries/work_packages/columns/relation_column.rb create mode 100644 spec/support/components/work_packages/columns.rb diff --git a/app/models/queries/work_packages/columns/relation_column.rb b/app/models/queries/work_packages/columns/relation_column.rb new file mode 100644 index 0000000000..bf79c121c7 --- /dev/null +++ b/app/models/queries/work_packages/columns/relation_column.rb @@ -0,0 +1,39 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::WorkPackages::Columns::RelationColumn < Queries::WorkPackages::Columns::WorkPackageColumn + attr_accessor :type + + def self.granted_by_enterprise_token + EnterpriseToken.allows_to?(:work_package_query_relation_columns) + end + + private_class_method :granted_by_enterprise_token +end diff --git a/app/models/queries/work_packages/columns/relation_of_type_column.rb b/app/models/queries/work_packages/columns/relation_of_type_column.rb index 4743e35a97..eb2331eea6 100644 --- a/app/models/queries/work_packages/columns/relation_of_type_column.rb +++ b/app/models/queries/work_packages/columns/relation_of_type_column.rb @@ -28,9 +28,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Columns::RelationOfTypeColumn < Queries::WorkPackages::Columns::WorkPackageColumn - attr_accessor :type - +class Queries::WorkPackages::Columns::RelationOfTypeColumn < Queries::WorkPackages::Columns::RelationColumn def initialize(type) super @@ -52,6 +50,8 @@ class Queries::WorkPackages::Columns::RelationOfTypeColumn < Queries::WorkPackag end def self.instances(_context = nil) + return [] unless granted_by_enterprise_token + Relation::TYPES.map { |_key, type| new(type) } end end diff --git a/app/models/queries/work_packages/columns/relation_to_type_column.rb b/app/models/queries/work_packages/columns/relation_to_type_column.rb index b5033a114f..b554ec1cd4 100644 --- a/app/models/queries/work_packages/columns/relation_to_type_column.rb +++ b/app/models/queries/work_packages/columns/relation_to_type_column.rb @@ -28,9 +28,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Columns::RelationToTypeColumn < Queries::WorkPackages::Columns::WorkPackageColumn - attr_accessor :type - +class Queries::WorkPackages::Columns::RelationToTypeColumn < Queries::WorkPackages::Columns::RelationColumn def initialize(type) super @@ -48,7 +46,9 @@ class Queries::WorkPackages::Columns::RelationToTypeColumn < Queries::WorkPackag end def self.instances(context = nil) - if context + if !granted_by_enterprise_token + [] + elsif context context.types else Type.all diff --git a/app/services/authorization/enterprise_service.rb b/app/services/authorization/enterprise_service.rb index 5d5ec9ee44..63bf727f1b 100644 --- a/app/services/authorization/enterprise_service.rb +++ b/app/services/authorization/enterprise_service.rb @@ -1,4 +1,5 @@ #-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2017 the OpenProject Foundation (OPF) @@ -30,6 +31,11 @@ class Authorization::EnterpriseService attr_accessor :token + GUARDED_ACTIONS = %i(define_custom_style + multiselect_custom_fields + edit_attribute_groups + work_package_query_relation_columns).freeze + def initialize(token) self.token = token end @@ -49,16 +55,8 @@ class Authorization::EnterpriseService private def process(action) - case action - when :define_custom_style - true # Every non-expired token - when :multiselect_custom_fields - true - when :edit_attribute_groups - true - else - false - end + # Every non-expired token + GUARDED_ACTIONS.include?(action) end def result(bool) diff --git a/spec/features/custom_fields/multi_value_custom_field_spec.rb b/spec/features/custom_fields/multi_value_custom_field_spec.rb index 8e1c2c6c76..84a8fbfed5 100644 --- a/spec/features/custom_fields/multi_value_custom_field_spec.rb +++ b/spec/features/custom_fields/multi_value_custom_field_spec.rb @@ -23,6 +23,7 @@ describe "multi select custom values", js: true do let(:wp_page) { Pages::FullWorkPackage.new work_package } let(:wp_table) { Pages::WorkPackagesTable.new project } let(:hierarchy) { ::Components::WorkPackages::Hierarchies.new } + let(:columns) { ::Components::WorkPackages::Columns.new } let(:user) { FactoryGirl.create :admin } context "with existing custom values" do @@ -93,7 +94,6 @@ describe "multi select custom values", js: true do field.field_type = 'select' field end - include_context 'work package table helpers' before do work_package @@ -104,7 +104,8 @@ describe "multi select custom values", js: true do wp_table.visit! wp_table.expect_work_package_listed(work_package) wp_table.expect_work_package_listed(work_package2) - add_wp_table_column(custom_field.name) + + columns.add custom_field.name end it 'should be usable in the table context' do diff --git a/spec/features/support/work_package_table.rb b/spec/features/support/work_package_table.rb index f72a25a71a..efe3f01491 100644 --- a/spec/features/support/work_package_table.rb +++ b/spec/features/support/work_package_table.rb @@ -27,33 +27,6 @@ #++ shared_context 'work package table helpers' do - def remove_wp_table_column(column_name) - click_button('Settings') - click_link('Columns ...') - - # This is faster than has_selector but does not wait for anything. - # So if problems occur, switch to has_selector? - if find('.select2-choices').text.include?(column_name) - find('.select2-search-choice', text: column_name) - .click_link('select2-search-choice-close') - end - - click_button('Apply') - end - - def add_wp_table_column(column_name) - click_button('Settings') - click_link('Columns ...') - - input = find '.select2-search-field input' - input.set column_name - - result = find '.select2-result-label' - result.click - - click_button('Apply') - end - def sort_wp_table_by(column_name, order: :desc) click_button('Settings') click_link('Sort by ...') diff --git a/spec/features/work_packages/table/relations_spec.rb b/spec/features/work_packages/table/relations_spec.rb index b2154cb93d..28fb03de9a 100644 --- a/spec/features/work_packages/table/relations_spec.rb +++ b/spec/features/work_packages/table/relations_spec.rb @@ -9,6 +9,7 @@ describe 'Work Package table relations', js: true do let(:wp_table) { Pages::WorkPackagesTable.new(project) } let(:relations) { ::Components::WorkPackages::Relations.new(relations) } + let(:columns) { ::Components::WorkPackages::Columns.new } let(:wp_timeline) { Pages::WorkPackagesTimeline.new(project) } let!(:wp_from) { FactoryGirl.create(:work_package, project: project, type: type2) } @@ -27,33 +28,42 @@ describe 'Work Package table relations', js: true do to: wp_to2, relation_type: Relation::TYPE_FOLLOWS) end + let!(:query) do + query = FactoryGirl.build(:query, user: user, project: project) + query.column_names = ['subject'] + query.filters.clear - before do - login_as(user) + query.save! + query end - describe 'relations can be displayed and expanded' do - include_context 'work package table helpers' + let(:type_column_id) { "relationsToType#{type.id}" } + let(:type_column_follows) { 'relationsOfTypeFollows' } + let(:relation_columns_allowed) { true } - let!(:query) do - query = FactoryGirl.build(:query, user: user, project: project) - query.column_names = ['subject'] - query.filters.clear + before do + # There does not seem to appear a way to generate a valid token + # for testing purposes + allow(EnterpriseToken) + .to receive(:allows_to?) + .and_return(false) - query.save! - query - end + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(relation_columns_allowed) - let(:type_column_id) { "relationsToType#{type.id}" } - let(:type_column_follows) { 'relationsOfTypeFollows' } + login_as(user) + end - it do + describe 'with relation columns allowed by the enterprise token' do + it 'displays expandable relation columns' do # Now visiting the query for category wp_table.visit_query(query) wp_table.expect_work_package_listed(wp_from, wp_to, wp_to2) - add_wp_table_column "Relations to #{type.name}" - add_wp_table_column "follows relations" + columns.add("Relations to #{type.name}") + columns.add("follows relations") wp_from_row = wp_table.row(wp_from) wp_from_to = wp_table.row(wp_to) @@ -100,4 +110,16 @@ describe 'Work Package table relations', js: true do wp_timeline.expect_row_count(4) end end + + describe 'with relation columns disallowed by the enterprise token' do + let(:relation_columns_allowed) { false } + + it 'has no relation columns available for selection' do + # Now visiting the query for category + wp_table.visit_query(query) + + columns.expect_column_not_available 'follows relations' + columns.expect_column_not_available "Relations to #{type.name}" + end + end end diff --git a/spec/features/work_packages/table_sorting_spec.rb b/spec/features/work_packages/table_sorting_spec.rb index 23022ad2f9..be7cea91d4 100644 --- a/spec/features/work_packages/table_sorting_spec.rb +++ b/spec/features/work_packages/table_sorting_spec.rb @@ -48,6 +48,7 @@ describe 'Select work package row', type: :feature do FactoryGirl.create(:version, project: project, name: 'zzz_version') end + let(:columns) { ::Components::WorkPackages::Columns.new } before do login_as(user) @@ -68,7 +69,7 @@ describe 'Select work package row', type: :feature do end it 'sorts by version although version is not selected as a column' do - remove_wp_table_column('Version') + columns.remove 'Version' sort_wp_table_by('Version') diff --git a/spec/models/enterprise_token_spec.rb b/spec/models/enterprise_token_spec.rb index 47f76c4d75..9608cba8f3 100644 --- a/spec/models/enterprise_token_spec.rb +++ b/spec/models/enterprise_token_spec.rb @@ -57,9 +57,13 @@ RSpec.describe EnterpriseToken, type: :model do end it 'forwards to EnterpriseTokenService for checks' do - expect(service_double).to receive(:call).with(:forbidden_action) + expect(service_double) + .to receive(:call) + .with(:forbidden_action) .and_return double('ServiceResult', result: false) - expect(service_double).to receive(:call).with(:allowed_action) + expect(service_double) + .to receive(:call) + .with(:allowed_action) .and_return double('ServiceResult', result: true) expect(EnterpriseToken.allows_to?(:forbidden_action)).to eq false @@ -102,11 +106,9 @@ RSpec.describe EnterpriseToken, type: :model do end describe "Configuration file has `ee_manager_visible` set to false" do - it 'does not show banners promoting EE' do expect(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(false) expect(EnterpriseToken.show_banners?).to be_falsey end end - end diff --git a/spec/models/queries/work_packages/columns/relation_of_type_column_spec.rb b/spec/models/queries/work_packages/columns/relation_of_type_column_spec.rb index af076ddaa1..29eff62a27 100644 --- a/spec/models/queries/work_packages/columns/relation_of_type_column_spec.rb +++ b/spec/models/queries/work_packages/columns/relation_of_type_column_spec.rb @@ -33,6 +33,7 @@ describe Queries::WorkPackages::Columns::RelationOfTypeColumn, type: :model do let(:project) { FactoryGirl.build_stubbed(:project) } let(:type) { FactoryGirl.build_stubbed(:type) } let(:instance) { described_class.new(type) } + let(:enterprise_token_allows) { true } it_behaves_like 'query column' @@ -41,17 +42,33 @@ describe Queries::WorkPackages::Columns::RelationOfTypeColumn, type: :model do stub_const('Relation::TYPES', relation1: { name: :label_relates_to, sym_name: :label_relates_to, order: 1, sym: :relation1 }, relation2: { name: :label_duplicates, sym_name: :label_duplicated_by, order: 2, sym: :relation2 }) + + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(enterprise_token_allows) end - it 'contains the type columns' do - expect(described_class.instances.length) - .to eq 2 + context 'with a valid enterprise token' do + it 'contains the type columns' do + expect(described_class.instances.length) + .to eq 2 + + expect(described_class.instances[0].sym) + .to eq :relation1 + + expect(described_class.instances[1].sym) + .to eq :relation2 + end + end - expect(described_class.instances[0].sym) - .to eq :relation1 + context 'without a valid enterprise token' do + let(:enterprise_token_allows) { false } - expect(described_class.instances[1].sym) - .to eq :relation2 + it 'is empty' do + expect(described_class.instances) + .to be_empty + end end end end diff --git a/spec/models/queries/work_packages/columns/relation_to_type_column_spec.rb b/spec/models/queries/work_packages/columns/relation_to_type_column_spec.rb index 215bedb98b..f7973d8546 100644 --- a/spec/models/queries/work_packages/columns/relation_to_type_column_spec.rb +++ b/spec/models/queries/work_packages/columns/relation_to_type_column_spec.rb @@ -33,10 +33,18 @@ describe Queries::WorkPackages::Columns::RelationToTypeColumn, type: :model do let(:project) { FactoryGirl.build_stubbed(:project) } let(:type) { FactoryGirl.build_stubbed(:type) } let(:instance) { described_class.new(type) } + let(:enterprise_token_allows) { true } it_behaves_like 'query column' describe 'instances' do + before do + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(enterprise_token_allows) + end + context 'within project' do before do allow(project) @@ -44,12 +52,23 @@ describe Queries::WorkPackages::Columns::RelationToTypeColumn, type: :model do .and_return([type]) end - it 'contains the type columns' do - expect(described_class.instances(project).length) - .to eq 1 + context 'with a valid enterprise token' do + it 'contains the type columns' do + expect(described_class.instances(project).length) + .to eq 1 + + expect(described_class.instances(project)[0].type) + .to eq type + end + end + + context 'without a valid enterprise token' do + let(:enterprise_token_allows) { false } - expect(described_class.instances(project)[0].type) - .to eq type + it 'is empty' do + expect(described_class.instances) + .to be_empty + end end end @@ -60,12 +79,23 @@ describe Queries::WorkPackages::Columns::RelationToTypeColumn, type: :model do .and_return([type]) end - it 'contains the type columns' do - expect(described_class.instances.length) - .to eq 1 + context 'with a valid enterprise token' do + it 'contains the type columns' do + expect(described_class.instances.length) + .to eq 1 + + expect(described_class.instances[0].type) + .to eq type + end + end + + context 'without a valid enterprise token' do + let(:enterprise_token_allows) { false } - expect(described_class.instances[0].type) - .to eq type + it 'is empty' do + expect(described_class.instances) + .to be_empty + end end end end diff --git a/spec/models/query_spec.rb b/spec/models/query_spec.rb index 3bdfa14ed6..34185e46af 100644 --- a/spec/models/query_spec.rb +++ b/spec/models/query_spec.rb @@ -31,6 +31,14 @@ require 'spec_helper' describe Query, type: :model do let(:query) { FactoryGirl.build(:query) } let(:project) { FactoryGirl.build_stubbed(:project) } + let(:relation_columns_allowed) { true } + + before do + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(relation_columns_allowed) + end describe '.new_default' do it 'set the default sortation' do @@ -176,6 +184,14 @@ describe Query, type: :model do it 'does not include the relation columns for types not in project' do expect(query.available_columns.map(&:name)).not_to include :"relations_to_type_#{type_not_in_project.id}" end + + context 'with the enterprise token disallowing relation columns' do + let(:relation_columns_allowed) { false } + + it 'excludes the relation columns' do + expect(query.available_columns.map(&:name)).not_to include :"relations_to_type_#{type_in_project.id}" + end + end end context 'global' do @@ -187,41 +203,83 @@ describe Query, type: :model do expect(query.available_columns.map(&:name)).to include(:"relations_to_type_#{type_in_project.id}", :"relations_to_type_#{type_not_in_project.id}") end + + context 'with the enterprise token disallowing relation columns' do + let(:relation_columns_allowed) { false } + + it 'excludes the relation columns' do + expect(query.available_columns.map(&:name)).not_to include(:"relations_to_type_#{type_in_project.id}", + :"relations_to_type_#{type_not_in_project.id}") + end + end end end context 'relation_of_type columns' do - it 'includes the relation columns for every relation type' do + before do stub_const('Relation::TYPES', relation1: { name: :label_relates_to, sym_name: :label_relates_to, order: 1, sym: :relation1 }, relation2: { name: :label_duplicates, sym_name: :label_duplicated_by, order: 2, sym: :relation2 }) + end + it 'includes the relation columns for every relation type' do expect(query.available_columns.map(&:name)).to include(:relations_of_type_relation1, :relations_of_type_relation2) end + + context 'with the enterprise token disallowing relation columns' do + let(:relation_columns_allowed) { false } + + it 'excludes the relation columns' do + expect(query.available_columns.map(&:name)).not_to include(:relations_of_type_relation1, + :relations_of_type_relation2) + end + end end end describe '.available_columns' do - it 'has all static columns, cf columns and relation columns' do - custom_field = FactoryGirl.create(:list_wp_custom_field) - - type = FactoryGirl.create(:type) + let(:custom_field) { FactoryGirl.create(:list_wp_custom_field) } + let(:type) { FactoryGirl.create(:type) } + before do stub_const('Relation::TYPES', relation1: { name: :label_relates_to, sym_name: :label_relates_to, order: 1, sym: :relation1 }, relation2: { name: :label_duplicates, sym_name: :label_duplicated_by, order: 2, sym: :relation2 }) + end + + context 'with the enterprise token allowing relation columns' do + it 'has all static columns, cf columns and relation columns' do + expected_columns = %i(id project assigned_to author + category created_at due_date estimated_hours + parent done_ratio priority responsible + spent_hours start_date status subject type + updated_at fixed_version) + + [:"cf_#{custom_field.id}"] + + [:"relations_to_type_#{type.id}"] + + %i(relations_of_type_relation1 relations_of_type_relation2) - expected_columns = %i(id project assigned_to author - category created_at due_date estimated_hours - parent done_ratio priority responsible - spent_hours start_date status subject type - updated_at fixed_version) + - [:"cf_#{custom_field.id}"] + - [:"relations_to_type_#{type.id}"] + - %i(relations_of_type_relation1 relations_of_type_relation2) + expect(Query.available_columns.map(&:name)).to include *expected_columns + end + end + + context 'with the enterprise token disallowing relation columns' do + let(:relation_columns_allowed) { false } - expect(Query.available_columns.map(&:name)).to include *expected_columns + it 'has all static columns, cf columns but no relation columns' do + expected_columns = %i(id project assigned_to author + category created_at due_date estimated_hours + parent done_ratio priority responsible + spent_hours start_date status subject type + updated_at fixed_version) + + [:"cf_#{custom_field.id}"] + + unexpected_columns = [:"relations_to_type_#{type.id}"] + + %i(relations_of_type_relation1 relations_of_type_relation2) + + expect(Query.available_columns.map(&:name)).to include *expected_columns + expect(Query.available_columns.map(&:name)).not_to include *unexpected_columns + end end end diff --git a/spec/requests/api/v3/queries/create_form_api_spec.rb b/spec/requests/api/v3/queries/create_form_api_spec.rb index 5acac95ed5..aef9bf8672 100644 --- a/spec/requests/api/v3/queries/create_form_api_spec.rb +++ b/spec/requests/api/v3/queries/create_form_api_spec.rb @@ -41,6 +41,55 @@ describe "POST /api/v3/queries/form", type: :request do let(:override_params) { {} } let(:form) { JSON.parse response.body } + let(:static_columns_json) do + %w(id project assignee author + category createdAt dueDate estimatedTime + parent percentageDone priority responsible + spentTime startDate status subject type + updatedAt version).map do |id| + { + '_type': 'QueryColumn::Property', + 'id': id + } + end + end + + let(:custom_field_columns_json) do + [ + { + '_type': 'QueryColumn::Property', + 'id': "customField#{custom_field.id}" + } + ] + end + + let(:relation_to_type_columns_json) do + project.types.map do |type| + { + '_type': 'QueryColumn::RelationToType', + 'id': "relationsToType#{type.id}" + } + end + end + + let(:relation_of_type_columns_json) do + Relation::TYPES.map do |_, value| + { + '_type': 'QueryColumn::RelationOfType', + 'id': "relationsOfType#{value[:sym].camelcase}" + } + end + end + + let(:non_project_type_relation_column_json) do + [ + { + '_type': 'QueryColumn::RelationToType', + 'id': "relationsToType#{non_project_type.id}" + } + ] + end + let(:additional_setup) {} before do @@ -106,6 +155,8 @@ describe "POST /api/v3/queries/form", type: :request do end describe 'columns' do + let(:relation_columns_allowed) { true } + let(:custom_field) do cf = FactoryGirl.create(:list_wp_custom_field) project.work_package_custom_fields << cf @@ -122,47 +173,63 @@ describe "POST /api/v3/queries/form", type: :request do custom_field non_project_type - end - it 'has the static, custom field and relation columns' do - expected_columns = (%w(id project assignee author - category createdAt dueDate estimatedTime - parent percentageDone priority responsible - spentTime startDate status subject type - updatedAt version) + ["customField#{custom_field.id}"]).map do |id| - { - '_type': 'QueryColumn::Property', - 'id': id - } - end + # There does not seem to appear a way to generate a valid token + # for testing purposes + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(relation_columns_allowed) + end - expected_columns += Type.all.map do |type| - { - '_type': 'QueryColumn::RelationToType', - 'id': "relationsToType#{type.id}" - } + context 'with relation columns allowed by the enterprise token' do + it 'has the static, custom field and relation columns' do + expected_columns = static_columns_json + + custom_field_columns_json + + relation_to_type_columns_json + + relation_of_type_columns_json + + non_project_type_relation_column_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include *expected_columns end + end - expected_columns += Relation::TYPES.map do |_, value| - { - '_type': 'QueryColumn::RelationOfType', - 'id': "relationsOfType#{value[:sym].camelcase}" - } + context 'with relation columns disallowed by the enterprise token' do + let(:relation_columns_allowed) { false } + + it 'has the static and custom field' do + expected_columns = static_columns_json + + custom_field_columns_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include *expected_columns + expect(actual_columns).not_to include(relation_to_type_columns_json) + expect(actual_columns).not_to include(relation_of_type_columns_json) + expect(actual_columns).not_to include(non_project_type_relation_column_json) end - - actual_columns = form.dig('_embedded', - 'schema', - 'columns', - '_embedded', - 'allowedValues') - .map do |column| - { - '_type': column['_type'], - 'id': column['id'] - } - end - - expect(actual_columns).to include *expected_columns end end end @@ -180,6 +247,8 @@ describe "POST /api/v3/queries/form", type: :request do end describe 'columns' do + let(:relation_columns_allowed) { true } + let(:custom_field) do cf = FactoryGirl.create(:list_wp_custom_field) project.work_package_custom_fields << cf @@ -196,53 +265,63 @@ describe "POST /api/v3/queries/form", type: :request do custom_field non_project_type - end - it 'has the static, custom field and relation columns' do - expected_columns = (%w(id project assignee author - category createdAt dueDate estimatedTime - parent percentageDone priority responsible - spentTime startDate status subject type - updatedAt version) + ["customField#{custom_field.id}"]).map do |id| - { - '_type': 'QueryColumn::Property', - 'id': id - } - end + # There does not seem to appear a way to generate a valid token + # for testing purposes + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(relation_columns_allowed) + end - expected_columns += project.types.map do |type| - { - '_type': 'QueryColumn::RelationToType', - 'id': "relationsToType#{type.id}" - } + context 'with relation columns allowed by the enterprise token' do + it 'has the static, custom field and relation columns' do + expected_columns = static_columns_json + + custom_field_columns_json + + relation_to_type_columns_json + + relation_of_type_columns_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include *expected_columns + expect(actual_columns).not_to include(non_project_type_relation_column_json) end + end - expected_columns += Relation::TYPES.map do |_, value| - { - '_type': 'QueryColumn::RelationOfType', - 'id': "relationsOfType#{value[:sym].camelcase}" - } + context 'with relation columns disallowed by the enterprise token' do + let(:relation_columns_allowed) { false } + + it 'has the static and custom field' do + expected_columns = static_columns_json + + custom_field_columns_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include *expected_columns + expect(actual_columns).not_to include(relation_to_type_columns_json) + expect(actual_columns).not_to include(relation_of_type_columns_json) + expect(actual_columns).not_to include(non_project_type_relation_column_json) end - - actual_columns = form.dig('_embedded', - 'schema', - 'columns', - '_embedded', - 'allowedValues') - .map do |column| - { - '_type': column['_type'], - 'id': column['id'] - } - end - - non_project_type_hash = { - '_type': 'QueryColumn::Relation', - 'id': "relationsToType#{non_project_type.id}" - } - - expect(actual_columns).to include *expected_columns - expect(actual_columns).not_to include(non_project_type_hash) end end end diff --git a/spec/requests/api/v3/queries/update_form_api_spec.rb b/spec/requests/api/v3/queries/update_form_api_spec.rb index add9454e2d..e2be0c1f79 100644 --- a/spec/requests/api/v3/queries/update_form_api_spec.rb +++ b/spec/requests/api/v3/queries/update_form_api_spec.rb @@ -112,6 +112,21 @@ describe "POST /api/v3/queries/form", type: :request do end describe 'columns' do + let(:relation_columns_allowed) { true } + + let(:additional_setup) do + custom_field + + non_project_type + + # There does not seem to appear a way to generate a valid token + # for testing purposes + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(relation_columns_allowed) + end + let(:custom_field) do cf = FactoryGirl.create(:list_wp_custom_field) project.work_package_custom_fields << cf @@ -124,58 +139,102 @@ describe "POST /api/v3/queries/form", type: :request do FactoryGirl.create(:type) end - let(:additional_setup) do - custom_field - - non_project_type + let(:static_columns_json) do + %w(id project assignee author + category createdAt dueDate estimatedTime + parent percentageDone priority responsible + spentTime startDate status subject type + updatedAt version).map do |id| + { + '_type': 'QueryColumn::Property', + 'id': id + } + end end - context 'within a project' do - it 'has the static, custom field and relation columns' do - expected_columns = (%w(id project assignee author - category createdAt dueDate estimatedTime - parent percentageDone priority responsible - spentTime startDate status subject type - updatedAt version) + ["customField#{custom_field.id}"]).map do |id| - { - '_type': 'QueryColumn::Property', - 'id': id - } - end + let(:custom_field_columns_json) do + [ + { + '_type': 'QueryColumn::Property', + 'id': "customField#{custom_field.id}" + } + ] + end - expected_columns += project.types.map do |type| - { - '_type': 'QueryColumn::RelationToType', - 'id': "relationsToType#{type.id}" - } - end + let(:relation_to_type_columns_json) do + project.types.map do |type| + { + '_type': 'QueryColumn::RelationToType', + 'id': "relationsToType#{type.id}" + } + end + end - expected_columns += Relation::TYPES.map do |_, value| - { - '_type': 'QueryColumn::RelationOfType', - 'id': "relationsOfType#{value[:sym].camelcase}" - } - end + let(:relation_of_type_columns_json) do + Relation::TYPES.map do |_, value| + { + '_type': 'QueryColumn::RelationOfType', + 'id': "relationsOfType#{value[:sym].camelcase}" + } + end + end - actual_columns = form.dig('_embedded', - 'schema', - 'columns', - '_embedded', - 'allowedValues') - .map do |column| - { - '_type': column['_type'], - 'id': column['id'] - } - end - - non_project_type_hash = { - '_type': 'QueryColumn::Relation', + let(:non_project_type_relation_column_json) do + [ + { + '_type': 'QueryColumn::RelationToType', 'id': "relationsToType#{non_project_type.id}" } + ] + end - expect(actual_columns).to include(*expected_columns) - expect(actual_columns).not_to include(non_project_type_hash) + context 'within a project' do + context 'with relation columns allowed by the enterprise token' do + it 'has the static, custom field and relation columns' do + expected_columns = static_columns_json + + custom_field_columns_json + + relation_to_type_columns_json + + relation_of_type_columns_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include(*expected_columns) + expect(actual_columns).not_to include(non_project_type_relation_column_json) + end + end + + context 'with relation columns disallowed by the enterprise token' do + it 'has the static and custom field' do + expected_columns = static_columns_json + + custom_field_columns_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include(*expected_columns) + expect(actual_columns).not_to include(non_project_type_relation_column_json) + expect(actual_columns).not_to include(relation_to_type_columns_json) + expect(actual_columns).not_to include(relation_of_type_columns_json) + end end end @@ -186,47 +245,61 @@ describe "POST /api/v3/queries/form", type: :request do non_project_type query.update_attribute(:project, nil) + + # There does not seem to appear a way to generate a valid token + # for testing purposes + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:work_package_query_relation_columns) + .and_return(relation_columns_allowed) end - it 'has the static, custom field and relation columns' do - expected_columns = (%w(id project assignee author - category createdAt dueDate estimatedTime - parent percentageDone priority responsible - spentTime startDate status subject type - updatedAt version) + ["customField#{custom_field.id}"]).map do |id| - { - '_type': 'QueryColumn::Property', - 'id': id - } - end - - expected_columns += Type.all.map do |type| - { - '_type': 'QueryColumn::RelationToType', - 'id': "relationsToType#{type.id}" - } + context 'with relation columns allowed by the enterprise token' do + it 'has the static, custom field and relation columns' do + expected_columns = static_columns_json + + custom_field_columns_json + + relation_to_type_columns_json + + non_project_type_relation_column_json + + relation_of_type_columns_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include(*expected_columns) end + end - expected_columns += Relation::TYPES.map do |_, value| - { - '_type': 'QueryColumn::RelationOfType', - 'id': "relationsOfType#{value[:sym].camelcase}" - } + context 'with relation columns disallowed by the enterprise token' do + it 'has the static, custom field and relation columns' do + expected_columns = static_columns_json + + custom_field_columns_json + + actual_columns = form.dig('_embedded', + 'schema', + 'columns', + '_embedded', + 'allowedValues') + .map do |column| + { + '_type': column['_type'], + 'id': column['id'] + } + end + + expect(actual_columns).to include(*expected_columns) + expect(actual_columns).not_to include(non_project_type_relation_column_json) + expect(actual_columns).not_to include(relation_to_type_columns_json) + expect(actual_columns).not_to include(relation_of_type_columns_json) end - - actual_columns = form.dig('_embedded', - 'schema', - 'columns', - '_embedded', - 'allowedValues') - .map do |column| - { - '_type': column['_type'], - 'id': column['id'] - } - end - - expect(actual_columns).to include(*expected_columns) end end end diff --git a/spec/services/authorization/enterprise_service_spec.rb b/spec/services/authorization/enterprise_service_spec.rb index 461ba67201..7d7893e4f4 100644 --- a/spec/services/authorization/enterprise_service_spec.rb +++ b/spec/services/authorization/enterprise_service_spec.rb @@ -73,13 +73,18 @@ describe Authorization::EnterpriseService do end end - context 'valid action requires active token' do - let(:action) { :define_custom_style } + %i(define_custom_style + multiselect_custom_fields + edit_attribute_groups + work_package_query_relation_columns).each do |guarded_action| + context "guarded action #{guarded_action}" do + let(:action) { guarded_action } - it 'returns a true result' do - expect(result).to be_kind_of ServiceResult - expect(result.result).to be_truthy - expect(result.success?).to be_truthy + it 'returns a true result' do + expect(result).to be_kind_of ServiceResult + expect(result.result).to be_truthy + expect(result.success?).to be_truthy + end end end end diff --git a/spec/support/components/work_packages/columns.rb b/spec/support/components/work_packages/columns.rb new file mode 100644 index 0000000000..a1b21530a6 --- /dev/null +++ b/spec/support/components/work_packages/columns.rb @@ -0,0 +1,110 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module Components + module WorkPackages + class Columns + include Capybara::DSL + include RSpec::Matchers + + def expect_column_available(name) + modal_open? or open_modal + + within_modal do + page.find('#selected_columns').click + + expect(page) + .to have_selector('li[role=option]', text: name) + end + end + + def expect_column_not_available(name) + modal_open? or open_modal + + within_modal do + page.find('#selected_columns').click + + expect(page) + .to have_no_selector('li[role=option]', text: name) + end + end + + def add(name) + modal_open? or open_modal + + within_modal do + input = find '.select2-search-field input' + input.set name + + result = find '.select2-result-label' + result.click + end + + apply + end + + def remove(name) + modal_open? or open_modal + + within_modal do + # This is faster than has_selector but does not wait for anything. + # So if problems occur, switch to has_selector? + if find('.select2-choices').text.include?(name) + find('.select2-search-choice', text: name) + .click_link('select2-search-choice-close') + end + end + + apply + end + + def apply + @opened = false + + click_button('Apply') + end + + def open_modal + @opened = true + SettingsMenu.new.open_and_choose('Columns ...') + end + + private + + def within_modal + page.within('.columns-modal') do + yield + end + end + + def modal_open? + !!@opened + end + end + end +end diff --git a/spec/views/layouts/base.html.erb_spec.rb b/spec/views/layouts/base.html.erb_spec.rb index d88e33ac05..aeed54d573 100644 --- a/spec/views/layouts/base.html.erb_spec.rb +++ b/spec/views/layouts/base.html.erb_spec.rb @@ -220,6 +220,7 @@ describe 'layouts/base', type: :view do context "EE does not allow custom styles" do before do allow(EnterpriseToken).to receive(:current).and_return(a_token) + allow(a_token).to receive(:expired?).and_return(false) allow(a_token).to receive(:allows_to?).with(:define_custom_style).and_return(false) render