diff --git a/Dockerfile b/Dockerfile index 47d677a69..2fa0a78d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.4.3 +FROM ruby:2.4.4 MAINTAINER BioSistemika # additional dependecies diff --git a/Gemfile b/Gemfile index 0f1ee73b3..751c9ceb7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'http://rubygems.org' -ruby '2.4.3' +ruby '2.4.4' gem 'rails', '5.1.6' gem 'webpacker', '~> 2.0' @@ -46,7 +46,7 @@ gem 'autosize-rails' # jQuery autosize plugin gem 'underscore-rails' gem 'turbolinks', '~> 5.1.1' -gem 'sdoc', '~> 0.4.0', group: :doc +gem 'sdoc', '~> 1.0', group: :doc gem 'bcrypt', '~> 3.1.10' gem 'logging', '~> 2.0.0' gem 'aspector' # Aspect-oriented programming for Rails diff --git a/Gemfile.lock b/Gemfile.lock index ffbc8c0e0..661bcdd0f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -399,7 +399,7 @@ GEM rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) - rdoc (4.3.0) + rdoc (6.0.4) recaptcha (4.6.3) json responders (2.4.0) @@ -461,9 +461,8 @@ GEM scss_lint (0.56.0) rake (>= 0.9, < 13) sass (~> 3.5.3) - sdoc (0.4.2) - json (~> 1.7, >= 1.7.7) - rdoc (~> 4.0) + sdoc (1.0.0) + rdoc (>= 5.0) shoulda-matchers (3.1.2) activesupport (>= 4.0.0) silencer (1.0.1) @@ -611,7 +610,7 @@ DEPENDENCIES sass-rails (~> 5.0.6) scenic (~> 1.4) scss_lint - sdoc (~> 0.4.0) + sdoc (~> 1.0) shoulda-matchers silencer simple_token_authentication (~> 1.15.1) @@ -631,7 +630,7 @@ DEPENDENCIES yomu RUBY VERSION - ruby 2.4.3p205 + ruby 2.4.4p296 BUNDLED WITH - 1.16.3 + 1.16.6 diff --git a/app/controllers/api/v1/inventory_cells_controller.rb b/app/controllers/api/v1/inventory_cells_controller.rb new file mode 100644 index 000000000..bef572803 --- /dev/null +++ b/app/controllers/api/v1/inventory_cells_controller.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Api + module V1 + class InventoryCellsController < BaseController + before_action :load_vars + before_action :load_inventory_cell, only: %i(show update destroy) + before_action :check_manage_permissions, only: %i(create update destroy) + + def index + cells = @inventory_item.repository_cells + .includes(Extends::REPOSITORY_SEARCH_INCLUDES) + .page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + render jsonapi: cells, each_serializer: InventoryCellSerializer + end + + def create + column = @inventory.repository_columns + .find(inventory_cell_params[:column_id]) + cell = RepositoryCell.create_with_value!(@inventory_item, + column, + inventory_cell_params[:value], + current_user) + render jsonapi: cell, + serializer: InventoryCellSerializer, + status: :created + end + + def show + render jsonapi: @inventory_cell, serializer: InventoryCellSerializer + end + + def update + value = update_inventory_cell_params[:value] + if @inventory_cell.value.data_changed?(value) + @inventory_cell.value.update_data!(value, current_user) + render jsonapi: @inventory_cell, serializer: InventoryCellSerializer + else + render body: nil, status: :no_content + end + end + + def destroy + @inventory_cell.destroy! + render body: nil + end + + private + + def load_vars + @team = Team.find(params.require(:team_id)) + unless can_read_team?(@team) + return render jsonapi: {}, status: :forbidden + end + @inventory = @team.repositories.find(params.require(:inventory_id)) + @inventory_item = @inventory.repository_rows.find(params[:item_id].to_i) + end + + def load_inventory_cell + @inventory_cell = @inventory_item.repository_cells + .find(params[:id].to_i) + end + + def check_manage_permissions + unless can_manage_repository_rows?(@team) + render body: nil, status: :forbidden + end + end + + def inventory_cell_params + unless params.require(:data).require(:type) == 'inventory_cells' + raise ActionController::BadRequest, + 'Wrong object type within parameters' + end + params.require(:data).require(:attributes).require(:column_id) + params.require(:data).require(:attributes).require(:value) + params[:data][:attributes] + end + + def update_inventory_cell_params + unless params.require(:data).require(:id).to_i == params[:id].to_i + raise ActionController::BadRequest, + 'Object ID mismatch in URL and request body' + end + inventory_cell_params + end + end + end +end diff --git a/app/models/repository_cell.rb b/app/models/repository_cell.rb index da502e3fa..8dea363f1 100644 --- a/app/models/repository_cell.rb +++ b/app/models/repository_cell.rb @@ -51,6 +51,7 @@ class RepositoryCell < ActiveRecord::Base cell.value = value value.save! end + cell end private diff --git a/app/serializers/api/v1/repository_asset_value_serializer.rb b/app/serializers/api/v1/repository_asset_value_serializer.rb index 50fac8350..35c576de7 100644 --- a/app/serializers/api/v1/repository_asset_value_serializer.rb +++ b/app/serializers/api/v1/repository_asset_value_serializer.rb @@ -23,9 +23,7 @@ module Api elsif object.asset&.file&.is_stored_on_s3? object.asset.presigned_url(download: true) else - # TODO - # separate api endpoint for local files download is needed - 'url'#download_asset_path(object.asset.id) + object.asset.file.url end end end diff --git a/config/routes.rb b/config/routes.rb index 65a86c2d7..5795cf580 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -559,7 +559,12 @@ Rails.application.routes.draw do resources :inventory_items, only: %i(index create show update destroy), path: 'items', - as: :items + as: :items do + resources :inventory_cells, + only: %i(index create show update destroy), + path: 'cells', + as: :cells + end end resources :projects, only: %i(index show) do resources :user_projects, only: %i(index show), diff --git a/spec/requests/api/v1/inventory_cells_controller_spec.rb b/spec/requests/api/v1/inventory_cells_controller_spec.rb new file mode 100644 index 000000000..22de816bc --- /dev/null +++ b/spec/requests/api/v1/inventory_cells_controller_spec.rb @@ -0,0 +1,455 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::InventoryCellsController', type: :request do + before :all do + @user = create(:user) + @team = create(:team, created_by: @user) + @wrong_team = create(:team, created_by: @user) + create(:user_team, user: @user, team: @team, role: 2) + + # valid_inventory + @valid_inventory = create(:repository, name: Faker::Name.unique.name, + created_by: @user, team: @team) + + # unaccessable_inventory + @wrong_inventory = create(:repository, name: Faker::Name.unique.name, + created_by: @user, team: @wrong_team) + create(:repository_row, repository: @wrong_inventory) + + @text_column = create(:repository_column, name: Faker::Name.unique.name, + repository: @valid_inventory, data_type: :RepositoryTextValue) + @list_column = create(:repository_column, name: Faker::Name.unique.name, + repository: @valid_inventory, data_type: :RepositoryListValue) + list_item = + create(:repository_list_item, repository: @valid_inventory, + repository_column: @list_column, data: Faker::Name.unique.name) + second_list_item = + create(:repository_list_item, repository: @valid_inventory, + repository_column: @list_column, data: Faker::Name.unique.name) + @file_column = create(:repository_column, name: Faker::Name.unique.name, + repository: @valid_inventory, data_type: :RepositoryAssetValue) + asset = create(:asset) + + @valid_item = create(:repository_row, repository: @valid_inventory) + + create(:repository_text_value, + data: Faker::Name.name, + repository_cell_attributes: + { repository_row: @valid_item, repository_column: @text_column }) + create(:repository_list_value, repository_list_item: list_item, + repository_cell_attributes: + { repository_row: @valid_item, repository_column: @list_column }) + create(:repository_asset_value, asset: asset, + repository_cell_attributes: + { repository_row: @valid_item, repository_column: @file_column }) + + @valid_headers = + { 'Authorization': 'Bearer ' + generate_token(@user.id), + 'Content-Type': 'application/json' } + + @valid_text_body = { + data: { + type: 'inventory_cells', + attributes: { + column_id: @text_column.id, + value: Faker::Name.unique.name + } + } + } + @valid_list_body = { + data: { + type: 'inventory_cells', + attributes: { + column_id: @list_column.id, + value: list_item.id + } + } + } + @valid_file_body = { + data: { + type: 'inventory_cells', + attributes: { + column_id: @file_column.id, + value: { + file_name: 'test.txt', + file_data: 'data:text/plain;base64,dGVzdAo=' + } + } + } + } + @update_text_body = { + data: { + id: @valid_item.repository_cells + .where(repository_column: @text_column).first.id, + type: 'inventory_cells', + attributes: { + column_id: @text_column.id, + value: Faker::Name.unique.name + } + } + } + @update_list_body = { + data: { + id: @valid_item.repository_cells + .where(repository_column: @list_column).first.id, + type: 'inventory_cells', + attributes: { + column_id: @list_column.id, + value: second_list_item.id + } + } + } + @update_file_body = { + data: { + id: @valid_item.repository_cells + .where(repository_column: @file_column).first.id, + type: 'inventory_cells', + attributes: { + column_id: @file_column.id, + value: { + file_name: 'test.txt', + file_data: 'data:text/plain;base64,dGVzdDIK=' + } + } + } + } + end + + describe 'GET inventory_cells, #index' do + it 'Response with correct inventory cells' do + hash_body = nil + get api_v1_team_inventory_item_cells_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id + ), headers: @valid_headers + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(@valid_item.repository_cells, + each_serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'When invalid request, user in not member of the team' do + hash_body = nil + get api_v1_team_inventory_item_cells_path( + team_id: @wrong_team.id, + inventory_id: @wrong_inventory.id, + item_id: 999 + ), headers: @valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, non existing item' do + hash_body = nil + get api_v1_team_inventory_item_cells_path( + team_id: @team.id, + inventory_id: @valid_inventory, + item_id: 999 + ), headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + end + + describe 'GET inventory_cells, #show' do + it 'Response with correct inventory cell' do + hash_body = nil + get api_v1_team_inventory_item_cell_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id, + id: @valid_item.repository_cells.first.id + ), headers: @valid_headers + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(@valid_item.repository_cells.first, + serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'When invalid request, user in not member of the team' do + hash_body = nil + get api_v1_team_inventory_item_cell_path( + team_id: @wrong_team.id, + inventory_id: @wrong_inventory.id, + item_id: 999, + id: 999 + ), headers: @valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, non existing cell' do + hash_body = nil + get api_v1_team_inventory_item_cell_path( + team_id: @team.id, + inventory_id: @valid_inventory, + item_id: @valid_item, + id: 999 + ), headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + end + + describe 'POST inventory_cells, #create' do + it 'Response with correct inventory cell, text cell' do + hash_body = nil + empty_item = create(:repository_row, repository: @valid_inventory) + post api_v1_team_inventory_item_cells_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: empty_item.id + ), params: @valid_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status 201 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(RepositoryCell.last, + serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'Response with correct inventory cell, list cell' do + hash_body = nil + empty_item = create(:repository_row, repository: @valid_inventory) + post api_v1_team_inventory_item_cells_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: empty_item.id + ), params: @valid_list_body.to_json, headers: @valid_headers + expect(response).to have_http_status 201 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(RepositoryCell.last, + serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'Response with correct inventory cell, file cell' do + hash_body = nil + empty_item = create(:repository_row, repository: @valid_inventory) + post api_v1_team_inventory_item_cells_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: empty_item.id + ), params: @valid_file_body.to_json, headers: @valid_headers + expect(response).to have_http_status 201 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(RepositoryCell.last, + serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'When invalid request, payload mismatches column type' do + hash_body = nil + invalid_file_body = @valid_file_body.dup + invalid_file_body[:data][:attributes][:value] = 'abc' + empty_item = create(:repository_row, repository: @valid_inventory) + post api_v1_team_inventory_item_cells_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: empty_item.id + ), params: invalid_file_body.to_json, headers: @valid_headers + expect(response).to have_http_status 400 + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, non existing inventory' do + hash_body = nil + post api_v1_team_inventory_item_cells_path( + team_id: @team.id, + inventory_id: -1, + item_id: @valid_item.id + ), params: @valid_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, user in not member of the team' do + hash_body = nil + post api_v1_team_inventory_item_cells_path( + team_id: @wrong_team.id, + inventory_id: @wrong_inventory.id, + item_id: 999 + ), params: @valid_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, repository from another team' do + hash_body = nil + post api_v1_team_inventory_items_path( + team_id: @team.id, + inventory_id: @wrong_inventory.id, + item_id: -1 + ), params: @valid_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + end + + describe 'PUT inventory_cells, #update' do + it 'Response with correct inventory cell, text cell' do + hash_body = nil + patch api_v1_team_inventory_item_cell_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id, + id: @valid_item.repository_cells + .where(repository_column: @text_column).first.id + ), params: @update_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status 200 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(@valid_item.repository_cells + .where(repository_column: @text_column).first, + serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'Response with correct inventory cell, list cell' do + hash_body = nil + patch api_v1_team_inventory_item_cell_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id, + id: @valid_item.repository_cells + .where(repository_column: @list_column).first.id + ), params: @update_list_body.to_json, headers: @valid_headers + expect(response).to have_http_status 200 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(@valid_item.repository_cells + .where(repository_column: @list_column).first, + serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'Response with correct inventory cell, file cell' do + hash_body = nil + patch api_v1_team_inventory_item_cell_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id, + id: @valid_item.repository_cells + .where(repository_column: @file_column).first.id + ), params: @update_file_body.to_json, headers: @valid_headers + expect(response).to have_http_status 200 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + ActiveModelSerializers::SerializableResource + .new(@valid_item.repository_cells + .where(repository_column: @file_column).first, + serializer: Api::V1::InventoryCellSerializer) + .as_json[:data] + ) + end + + it 'When invalid request, payload mismatches column type' do + hash_body = nil + invalid_file_body = @valid_file_body.dup + invalid_file_body[:data][:attributes][:value] = 'abc' + patch api_v1_team_inventory_item_cell_path( + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id, + id: @valid_item.repository_cells + .where(repository_column: @file_column).first.id + ), params: invalid_file_body.to_json, headers: @valid_headers + expect(response).to have_http_status 400 + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, non existing inventory' do + hash_body = nil + patch api_v1_team_inventory_item_cell_path( + team_id: @team.id, + inventory_id: -1, + item_id: @valid_item.id, + id: -1 + ), params: @valid_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, user in not member of the team' do + hash_body = nil + patch api_v1_team_inventory_item_cell_path( + team_id: @wrong_team.id, + inventory_id: @wrong_inventory.id, + item_id: -1, + id: -1 + ), params: @valid_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + + it 'When invalid request, repository from another team' do + hash_body = nil + patch api_v1_team_inventory_item_path( + team_id: @team.id, + inventory_id: @wrong_inventory.id, + item_id: -1, + id: -1 + ), params: @valid_text_body.to_json, headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body).to match({}) + end + end + + describe 'DELETE inventory_cells, #destroy' do + it 'Destroys inventory cell' do + deleted_id = @valid_item.repository_cells.last.id + delete api_v1_team_inventory_item_cell_path( + id: deleted_id, + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id + ), headers: @valid_headers + expect(response).to have_http_status(200) + expect(RepositoryCell.where(id: deleted_id)).to_not exist + end + + it 'Invalid request, non existing inventory item' do + deleted_id = RepositoryCell.last.id + 1 + delete api_v1_team_inventory_item_cell_path( + id: deleted_id, + team_id: @team.id, + inventory_id: @valid_inventory.id, + item_id: @valid_item.id + ), headers: @valid_headers + expect(response).to have_http_status(404) + end + end +end