diff --git a/app/controllers/api/generic_object_definitions_controller.rb b/app/controllers/api/generic_object_definitions_controller.rb index b5a60afeb6..32d249e4d7 100644 --- a/app/controllers/api/generic_object_definitions_controller.rb +++ b/app/controllers/api/generic_object_definitions_controller.rb @@ -8,8 +8,7 @@ def create_resource(_type, _id, data) end def edit_resource(type, id, data) - id ||= data['name'] - go_def = resource_search(id, type, collection_class(:generic_object_definitions)) + go_def = fetch_generic_object_definition(type, id, data) updated_data = data['resource'].try(:deep_symbolize_keys) || data.deep_symbolize_keys go_def.update_attributes!(updated_data) if data.present? go_def @@ -18,13 +17,78 @@ def edit_resource(type, id, data) end def delete_resource(type, id, data = {}) - id ||= data['name'] - go_def = resource_search(id, type, collection_class(:generic_object_definitions)) + go_def = fetch_generic_object_definition(type, id, data) go_def.destroy! rescue => err raise BadRequestError, "Failed to delete generic object definition - #{err}" end + def add_attributes_resource(type, id, data) + go_def = fetch_generic_object_definition(type, id, data) + attributes = data['attributes'] || data['resource']['attributes'] + attributes.each do |name, attribute_type| + go_def.add_property_attribute(name, attribute_type) + end + go_def + rescue => err + raise BadRequestError, "Failed to add attributes to generic object definition - #{err}" + end + + def remove_attributes_resource(type, id, data) + go_def = fetch_generic_object_definition(type, id, data) + attributes = data['attributes'] || data['resource']['attributes'] + attributes.each do |name, _type| + go_def.delete_property_attribute(name) + end + go_def + rescue => err + raise BadRequestError, "Failed to remove attributes from generic object definition - #{err}" + end + + def add_associations_resource(type, id, data) + go_def = fetch_generic_object_definition(type, id, data) + associations = data['associations'] || data['resource']['associations'] + associations.each do |name, association_type| + go_def.add_property_association(name, association_type) + end + go_def + rescue => err + raise BadRequestError, "Failed to add attributes to object definition - #{err}" + end + + def remove_associations_resource(type, id, data) + go_def = fetch_generic_object_definition(type, id, data) + associations = data['associations'] || data['resource']['associations'] + associations.each do |name, _type| + go_def.delete_property_association(name) + end + go_def + rescue => err + raise BadRequestError, "Failed to add attributes to object definition - #{err}" + end + + def add_methods_resource(type, id, data) + go_def = fetch_generic_object_definition(type, id, data) + methods = data['methods'] || data['resource']['methods'] + methods.each do |name| + go_def.add_property_method(name) + end + go_def + rescue => err + raise BadRequestError, "Failed to add attributes to object definition - #{err}" + end + + def remove_methods_resource(type, id, data) + go_def = fetch_generic_object_definition(type, id, data) + methods = data['methods'] || data['resource']['methods'] + methods.each do |name| + go_def.delete_property_method(name) + end + go_def + rescue => err + raise BadRequestError, "Failed to add attributes to object definition - #{err}" + end + def self.allowed_association_types GenericObjectDefinition::ALLOWED_ASSOCIATION_TYPES.collect do |association_type| [Dictionary.gettext(association_type, :type => :model, :notfound => :titleize, :plural => false), association_type] @@ -47,8 +111,14 @@ def build_generic_object_definition_options :allowed_types => GenericObjectDefinitionsController.allowed_types } end + private + def fetch_generic_object_definition(type, id, data) + id ||= data['name'] + resource_search(id, type, collection_class(type)) + end + def resource_search(id, type, klass) if ApplicationRecord.compressed_id?(id) super diff --git a/config/api.yml b/config/api.yml index 2950faf4ab..a2143df84b 100644 --- a/config/api.yml +++ b/config/api.yml @@ -842,6 +842,18 @@ :identifier: generic_object_definition_edit - :name: delete :identifier: generic_object_definition_delete + - :name: add_attributes + :identifier: generic_object_definition_edit + - :name: remove_attributes + :identifier: generic_object_definition_edit + - :name: add_associations + :identifier: generic_object_definition_edit + - :name: remove_associations + :identifier: generic_object_definition_edit + - :name: add_methods + :identifier: generic_object_definition_edit + - :name: remove_methods + :identifier: generic_object_definition_edit :resource_actions: :get: - :name: read @@ -851,6 +863,18 @@ :identifier: generic_object_definition_edit - :name: delete :identifier: generic_object_definition_delete + - :name: add_attributes + :identifier: generic_object_definition_edit + - :name: remove_attributes + :identifier: generic_object_definition_edit + - :name: add_associations + :identifier: generic_object_definition_edit + - :name: remove_associations + :identifier: generic_object_definition_edit + - :name: add_methods + :identifier: generic_object_definition_edit + - :name: remove_methods + :identifier: generic_object_definition_edit :put: - :name: edit :identifier: generic_object_definition_edit diff --git a/spec/requests/generic_object_definitions_spec.rb b/spec/requests/generic_object_definitions_spec.rb index 5567f87395..30265378db 100644 --- a/spec/requests/generic_object_definitions_spec.rb +++ b/spec/requests/generic_object_definitions_spec.rb @@ -1,6 +1,8 @@ # rubocop:disable Style/WordArray RSpec.describe 'GenericObjectDefinitions API' do let(:object_def) { FactoryGirl.create(:generic_object_definition, :name => 'foo') } + let(:object_def2) { FactoryGirl.create(:generic_object_definition, :name => 'foo 2') } + let(:object_def3) { FactoryGirl.create(:generic_object_definition, :name => 'foo 3') } describe 'GET /api/generic_object_definitions' do it 'does not list object definitions without an appropriate role' do @@ -127,8 +129,6 @@ end it 'can edit generic_object_definitions by id, name, or href' do - object_def2 = FactoryGirl.create(:generic_object_definition, :name => 'foo 2') - object_def3 = FactoryGirl.create(:generic_object_definition, :name => 'foo 3') api_basic_authorize collection_action_identifier(:generic_object_definitions, :edit) request = { @@ -151,6 +151,172 @@ expect(response).to have_http_status(:ok) expect(response.parsed_body).to include(expected) end + + it 'can add associations by id, name, or href' do + api_basic_authorize collection_action_identifier(:generic_object_definitions, :add_associations) + + request = { + 'action' => 'add_associations', + 'resources' => [ + { 'name' => object_def.name, 'resource' => { 'associations' => { 'association1' => 'AvailabilityZone' } } }, + { 'id' => object_def2.compressed_id, 'resource' => { 'associations' => { 'association2' => 'AvailabilityZone' } }}, + { 'href' => api_generic_object_definition_url(nil, object_def3.compressed_id), 'resource' => { 'associations' => { 'association3' => 'AvailabilityZone' } }} + ] + } + post(api_generic_object_definitions_url, :params => request) + + expected = { + 'results' => a_collection_including( + a_hash_including('id' => object_def.compressed_id, 'properties' => a_hash_including('associations' => {'association1' => 'AvailabilityZone'})), + a_hash_including('id' => object_def2.compressed_id, 'properties' => a_hash_including('associations' => {'association2' => 'AvailabilityZone'})), + a_hash_including('id' => object_def3.compressed_id, 'properties' => a_hash_including('associations' => {'association3' => 'AvailabilityZone'})) + ) + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to include(expected) + end + + it 'cannot add associations without an appropriate role' do + api_basic_authorize + + post(api_generic_object_definitions_url, :params => { :action => 'add_associations' }) + + expect(response).to have_http_status(:forbidden) + end + + it 'can remove associations by id, name, or href' do + object_def.add_property_association('association1', 'AvailabilityZone') + object_def2.add_property_association('association2', 'AvailabilityZone') + object_def3.add_property_association('association3', 'AvailabilityZone') + api_basic_authorize collection_action_identifier(:generic_object_definitions, :add_associations) + + request = { + 'action' => 'remove_associations', + 'resources' => [ + { 'name' => object_def.name, 'resource' => { 'associations' => { 'association1' => 'AvailabilityZone' } } }, + { 'id' => object_def2.compressed_id, 'resource' => { 'associations' => { 'association2' => 'AvailabilityZone' } }}, + { 'href' => api_generic_object_definition_url(nil, object_def3.compressed_id), 'resource' => { 'associations' => { 'association3' => 'AvailabilityZone' } }} + ] + } + post(api_generic_object_definitions_url, :params => request) + + expected = { + 'results' => a_collection_including( + a_hash_including('id' => object_def.compressed_id, 'properties' => a_hash_including('associations' => {})), + a_hash_including('id' => object_def2.compressed_id, 'properties' => a_hash_including('associations' => {})), + a_hash_including('id' => object_def3.compressed_id, 'properties' => a_hash_including('associations' => {})) + ) + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to include(expected) + end + + it 'cannot remove associations without an appropriate role' do + api_basic_authorize + + post(api_generic_object_definitions_url, :params => { :action => 'remove_associations' }) + + expect(response).to have_http_status(:forbidden) + end + + it 'can add attributes by id, name, or href' do + api_basic_authorize collection_action_identifier(:generic_object_definitions, :add_associations) + + request = { + 'action' => 'add_attributes', + 'resources' => [ + { 'name' => object_def.name, 'resource' => { 'attributes' => { 'attr1' => 'string' } } }, + { 'id' => object_def2.compressed_id, 'resource' => { 'attributes' => { 'attr2' => 'string' } }}, + { 'href' => api_generic_object_definition_url(nil, object_def3.compressed_id), 'resource' => { 'attributes' => { 'attr3' => 'string' } }} + ] + } + post(api_generic_object_definitions_url, :params => request) + + expected = { + 'results' => a_collection_including( + a_hash_including('id' => object_def.compressed_id, 'properties' => a_hash_including('attributes' => {'attr1' => 'string'})), + a_hash_including('id' => object_def2.compressed_id, 'properties' => a_hash_including('attributes' => {'attr2' => 'string'})), + a_hash_including('id' => object_def3.compressed_id, 'properties' => a_hash_including('attributes' => {'attr3' => 'string'})) + ) + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to include(expected) + end + + it 'cannot add attributes without an appropriate role' do + api_basic_authorize + + post(api_generic_object_definitions_url, :params => { :action => 'add_attributes' }) + + expect(response).to have_http_status(:forbidden) + end + + it 'can remove attributes by id, name, or href' do + object_def.add_property_attribute('attr1', 'string') + object_def2.add_property_attribute('attr2', 'string') + object_def3.add_property_attribute('attr3', 'string') + api_basic_authorize collection_action_identifier(:generic_object_definitions, :add_associations) + + request = { + 'action' => 'remove_attributes', + 'resources' => [ + { 'name' => object_def.name, 'resource' => { 'attributes' => { 'attr1' => 'string' } } }, + { 'id' => object_def2.compressed_id, 'resource' => { 'attributes' => { 'attr2' => 'string' } }}, + { 'href' => api_generic_object_definition_url(nil, object_def3.compressed_id), 'resource' => { 'attributes' => { 'attr3' => 'string' } }} + ] + } + post(api_generic_object_definitions_url, :params => request) + + expected = { + 'results' => a_collection_including( + a_hash_including('id' => object_def.compressed_id, 'properties' => a_hash_including('attributes' => {})), + a_hash_including('id' => object_def2.compressed_id, 'properties' => a_hash_including('attributes' => {})), + a_hash_including('id' => object_def3.compressed_id, 'properties' => a_hash_including('attributes' => {})) + ) + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to include(expected) + end + + it 'cannot remove attributes without an appropriate role' do + api_basic_authorize + + post(api_generic_object_definitions_url, :params => { :action => 'remove_attributes' }) + + expect(response).to have_http_status(:forbidden) + end + + it 'can add methods by id, name, or href' do + api_basic_authorize collection_action_identifier(:generic_object_definitions, :add_associations) + + request = { + 'action' => 'add_methods', + 'resources' => [ + { 'name' => object_def.name, 'resource' => { 'methods' => ['method1'] } }, + { 'id' => object_def2.compressed_id, 'resource' => { 'methods' => ['method2'] }}, + { 'href' => api_generic_object_definition_url(nil, object_def3.compressed_id), 'resource' => { 'methods' => ['method3'] }} + ] + } + post(api_generic_object_definitions_url, :params => request) + + expected = { + 'results' => a_collection_including( + a_hash_including('id' => object_def.compressed_id, 'properties' => a_hash_including('methods' => ['method1'])), + a_hash_including('id' => object_def2.compressed_id, 'properties' => a_hash_including('methods' => ['method2'])), + a_hash_including('id' => object_def3.compressed_id, 'properties' => a_hash_including('methods' => ['method3'])) + ) + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to include(expected) + end + + it 'cannot add methods without an appropriate role' do + api_basic_authorize + + post(api_generic_object_definitions_url, :params => { :action => 'add_methods' }) + + expect(response).to have_http_status(:forbidden) + end end describe 'POST /api/generic_object_definitions/:id' do @@ -266,8 +432,6 @@ end it 'can delete generic_object_definition in bulk by name, id, or href' do - object_def2 = FactoryGirl.create(:generic_object_definition, :name => 'foo 2') - object_def3 = FactoryGirl.create(:generic_object_definition, :name => 'foo 3') api_basic_authorize collection_action_identifier(:generic_object_definitions, :delete) request = { @@ -290,6 +454,191 @@ expect(response).to have_http_status(:ok) expect(response.parsed_body).to include(expected) end + + it 'can add new attributes to the resource' do + api_basic_authorize action_identifier(:generic_object_definitions, :add_attributes) + + request = { + 'action' => 'add_attributes', + 'resource' => { + 'attributes' => { + 'added_attribute1' => 'string', + 'added_attribute2' => 'boolean' + } + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'attributes' => { + 'added_attribute1' => 'string', + 'added_attribute2' => 'boolean' + } + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body['properties']).to include(expected) + end + + it 'will raise an error for a bad attribute' do + api_basic_authorize action_identifier(:generic_object_definitions, :add_attributes) + + request = { + 'action' => 'add_attributes', + 'resource' => { + 'attributes' => { + 'added_attribute1' => 'bad_val' + } + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'error' => a_hash_including( + 'kind' => 'bad_request', + 'message' => a_string_including('Failed to add attributes') + ) + } + expect(response).to have_http_status(:bad_request) + expect(response.parsed_body).to include(expected) + end + + it 'can remove property attributes' do + object_def.add_property_attribute('foo', 'string') + object_def.add_property_attribute('bar', 'boolean') + api_basic_authorize action_identifier(:generic_object_definitions, :remove_attributes) + + request = { + 'action' => 'remove_attributes', + 'resource' => { + 'attributes' => { + 'foo' => 'string' + } + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'attributes' => { + 'bar' => 'boolean' + } + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body['properties']).to include(expected) + end + + it 'can add new methods' do + api_basic_authorize action_identifier(:generic_object_definitions, :add_methods) + + request = { + 'action' => 'add_methods', + 'resource' => { + 'methods' => ['foo', 'bar'] + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'methods' => ['foo', 'bar'] + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body['properties']).to include(expected) + end + + it 'can remove methods' do + object_def.add_property_method('foo') + object_def.add_property_method('bar') + api_basic_authorize action_identifier(:generic_object_definitions, :remove_methods) + + request = { + 'action' => 'remove_methods', + 'resource' => { + 'methods' => ['foo'] + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'methods' => ['bar'] + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body['properties']).to include(expected) + end + + it 'cannot remove methods without an appropriate role' do + api_basic_authorize + + post(api_generic_object_definitions_url, :params => { :action => 'remove_methods'}) + + expect(response).to have_http_status(:forbidden) + end + + it 'can add associations' do + api_basic_authorize action_identifier(:generic_object_definitions, :add_associations) + + request = { + 'action' => 'add_associations', + 'resource' => { + 'associations' => { + 'az' => 'AvailabilityZone', + 'chargeback' => 'ChargebackVm' + } + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'associations' => { + 'az' => 'AvailabilityZone', + 'chargeback' => 'ChargebackVm' + } + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body['properties']).to include(expected) + end + + it 'will raise an error for invalid associations' do + api_basic_authorize action_identifier(:generic_object_definitions, :add_associations) + + request = { + 'action' => 'add_associations', + 'resource' => { + 'associations' => { + 'az' => 'AvailabilityZone', + 'chargeback' => 'bad_type' + } + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'error' => a_hash_including( + 'kind' => 'bad_request', + 'message' => a_string_including('Failed to add attributes') + ) + } + expect(response).to have_http_status(:bad_request) + expect(response.parsed_body).to include(expected) + end + + it 'can remove associations' do + object_def.add_property_association('az', 'AvailabilityZone') + object_def.add_property_association('chargeback', 'ChargebackVm') + api_basic_authorize action_identifier(:generic_object_definitions, :remove_associations) + + request = { + 'action' => 'remove_associations', + 'resource' => { + 'associations' => { 'az' => 'AvailabilityZone' } + } + } + post(api_generic_object_definition_url(nil, object_def.compressed_id), :params => request) + + expected = { + 'associations' => { 'chargeback' => 'ChargebackVm' } + } + expect(response).to have_http_status(:ok) + expect(response.parsed_body['properties']).to include(expected) + end end describe 'DELETE /api/generic_object_definitions/:id' do