diff --git a/app/controllers/api/v1/tables_controller.rb b/app/controllers/api/v1/tables_controller.rb index 60dcd40df..b9f5d3736 100644 --- a/app/controllers/api/v1/tables_controller.rb +++ b/app/controllers/api/v1/tables_controller.rb @@ -88,9 +88,13 @@ module Api metadata_cells = metadata[:cells] data = contents['data'] - if data.present? && data[0].present? && (data.size * data[0].size) < metadata_cells.size - error_message = I18n.t('api.core.errors.table.metadata.detail_too_many_cells') - raise ActionController::BadRequest, error_message + if data.present? && data[0].present? + data_size = (data[0].is_a?(Array) ? data.size * data[0].size : data.size) + + if data_size < metadata_cells.size + error_message = I18n.t('api.core.errors.table.metadata.detail_too_many_cells') + raise ActionController::BadRequest, error_message + end end end end diff --git a/app/controllers/api/v2/base_controller.rb b/app/controllers/api/v2/base_controller.rb new file mode 100644 index 000000000..34cf12c29 --- /dev/null +++ b/app/controllers/api/v2/base_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Api + module V2 + class BaseController < Api::V1::BaseController + private + + def load_result(key = :result_id) + @result = @task.results.find(params.require(key)) + + raise PermissionError.new(Result, :read) unless can_read_result?(@result) + end + + def load_result_text(key = :result_text_id) + @result_text = @result.result_texts.find(params.require(key)) + raise PermissionError.new(Result, :read) unless can_read_result?(@result) + end + + def load_result_table(key = :table_id) + @table = @result.tables.find(params.require(key)) + raise PermissionError.new(Result, :read) unless can_read_result?(@result) + end + end + end +end diff --git a/app/controllers/api/v2/result_assets_controller.rb b/app/controllers/api/v2/result_assets_controller.rb new file mode 100644 index 000000000..fe5aa256b --- /dev/null +++ b/app/controllers/api/v2/result_assets_controller.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultAssetsController < BaseController + before_action :load_team, :load_project, :load_experiment, :load_task, :load_result + before_action :check_manage_permission, only: %i(create destroy) + before_action :load_asset, only: %i(show destroy) + before_action :check_upload_type, only: :create + + def index + result_assets = + timestamps_filter(@result.result_assets).page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + + render jsonapi: result_assets, each_serializer: ResultAssetSerializer + end + + def show + render jsonapi: @asset.result_asset, serializer: ResultAssetSerializer + end + + def create + asset = if @form_multipart_upload + @result.assets.new(asset_params.merge({ team_id: @team.id })) + else + blob = ActiveStorage::Blob.create_and_upload!( + io: StringIO.new(Base64.decode64(asset_params[:file_data])), + filename: asset_params[:file_name], + content_type: asset_params[:file_type] + ) + @result.assets.new(file: blob, team: @team) + end + + asset.save!(context: :on_api_upload) + asset.post_process_file + + render jsonapi: asset.result_asset, + serializer: ResultAssetSerializer, + status: :created + end + + def destroy + @asset.destroy! + render body: nil + end + + private + + def asset_params + raise TypeError unless params.require(:data).require(:type) == 'attachments' + + return params.require(:data).require(:attributes).permit(:file) if @form_multipart_upload + + attr_list = %i(file_data file_type file_name) + params.require(:data).require(:attributes).require(attr_list) + params.require(:data).require(:attributes).permit(attr_list) + end + + def load_asset + @asset = @result.assets.find(params.require(:id)) + raise PermissionError.new(Result, :read) unless can_read_result?(@result) + end + + def check_upload_type + @form_multipart_upload = true if params.dig(:data, :attributes, :file) + end + + def check_manage_permission + raise PermissionError.new(Result, :manage) unless can_manage_result?(@result) + end + end + end +end diff --git a/app/controllers/api/v2/result_tables_controller.rb b/app/controllers/api/v2/result_tables_controller.rb new file mode 100644 index 000000000..139861f4e --- /dev/null +++ b/app/controllers/api/v2/result_tables_controller.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultTablesController < BaseController + before_action :load_team, :load_project, :load_experiment, :load_task, :load_result + before_action only: %i(show update destroy) do + load_result_table(:id) + end + before_action :check_manage_permission, only: %i(create update destroy) + + def index + result_tables = timestamps_filter(@result.result_tables).page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + + render jsonapi: result_tables, each_serializer: ResultTableSerializer + end + + def show + render jsonapi: @table.result_table, serializer: ResultTableSerializer + end + + def create + table = @result.tables.new(table_params.merge!(team: @team, created_by: current_user)) + + @result.with_lock do + @result.result_orderable_elements.create!( + position: @result.result_orderable_elements.size, + orderable: table.result_table + ) + + table.save! + end + + render jsonapi: table.result_table, serializer: ResultTableSerializer, status: :created + end + + def update + @table.assign_attributes(table_params) + + if @table.changed? && @table.save! + render jsonapi: @table.result_table, serializer: ResultTableSerializer + else + render body: nil, status: :no_content + end + end + + def destroy + @table.destroy! + render body: nil + end + + private + + def check_manage_permission + raise PermissionError.new(Result, :manage) unless can_manage_result?(@result) + end + + def convert_plate_template(metadata_params) + if metadata_params.present? && metadata_params['plateTemplate'] + metadata_params['plateTemplate'] = ActiveRecord::Type::Boolean.new.cast(metadata_params['plateTemplate']) + end + end + + def table_params + raise TypeError unless params.require(:data).require(:type) == 'tables' + + attributes_params = params.require(:data).require(:attributes).permit( + :name, + :contents, + metadata: [ + :plateTemplate, + { cells: %i(col row className) } + ] + ) + + convert_plate_template(attributes_params[:metadata]) + validate_metadata_params(attributes_params) + attributes_params + end + + def validate_metadata_params(attributes_params) + metadata = attributes_params[:metadata] + contents = JSON.parse(attributes_params[:contents] || '{}') + + if metadata.present? && metadata[:cells].present? && contents.present? + metadata_cells = metadata[:cells] + data = contents['data'] + + if data.present? && data[0].present? + data_size = (data[0].is_a?(Array) ? data.size * data[0].size : data.size) + + if data_size < metadata_cells.size + error_message = I18n.t('api.core.errors.table.metadata.detail_too_many_cells') + raise ActionController::BadRequest, error_message + end + end + end + end + end + end +end diff --git a/app/controllers/api/v2/result_texts_controller.rb b/app/controllers/api/v2/result_texts_controller.rb new file mode 100644 index 000000000..4b14c6232 --- /dev/null +++ b/app/controllers/api/v2/result_texts_controller.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultTextsController < BaseController + before_action :load_team, :load_project, :load_experiment, :load_task, :load_result + before_action only: %i(show update destroy) do + load_result_text(:id) + end + before_action :check_manage_permission, only: %i(create update destroy) + + def index + result_texts = timestamps_filter(@result.result_texts).page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + + render jsonapi: result_texts, each_serializer: ResultTextSerializer + end + + def show + render jsonapi: @result_text, serializer: ResultTextSerializer + end + + def create + result_text = @result.result_texts.new(result_text_params) + + @result.with_lock do + @result.result_orderable_elements.create!( + position: @result.result_orderable_elements.size, + orderable: result_text + ) + + result_text.save! + end + + render jsonapi: result_text, serializer: ResultTextSerializer, status: :created + end + + def update + @result_text.assign_attributes(result_text_params) + + if @result_text.changed? && @result_text.save! + render jsonapi: @result_text, serializer: ResultTextSerializer, status: :ok + else + render body: nil, status: :no_content + end + end + + def destroy + @result_text.destroy! + render body: nil + end + + private + + def check_manage_permission + raise PermissionError.new(Result, :manage) unless can_manage_result?(@result) + end + + def result_text_params + raise TypeError unless params.require(:data).require(:type) == 'result_texts' + + params.require(:data).require(:attributes).permit(:text, :name) + end + end + end +end diff --git a/app/controllers/api/v2/results_controller.rb b/app/controllers/api/v2/results_controller.rb new file mode 100644 index 000000000..42221d50c --- /dev/null +++ b/app/controllers/api/v2/results_controller.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultsController < BaseController + before_action :load_team, :load_project, :load_experiment, :load_task + before_action only: %i(show update destroy) do + load_result(:id) + end + before_action :check_create_permissions, only: :create + before_action :check_delete_permissions, only: :destroy + before_action :check_update_permissions, only: :update + + def index + results = timestamps_filter(@task.results).page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + render jsonapi: results, each_serializer: ResultSerializer, + include: include_params + end + + def show + render jsonapi: @result, serializer: ResultSerializer, + include: include_params + end + + def create + @result = Result.create!( + user: current_user, + my_module: @task, + name: result_params[:name] + ) + render jsonapi: @result, serializer: ResultSerializer + end + + def update + @result.assign_attributes(result_params) + + if @result.changed? && @result.save! + render jsonapi: @result, serializer: ResultSerializer + else + render body: nil, status: :no_content + end + end + + def destroy + @result.destroy! + render body: nil + end + + private + + def check_create_permissions + raise PermissionError.new(MyModule, :manage) unless can_manage_my_module?(@task) + end + + def check_delete_permissions + raise PermissionError.new(Result, :delete) unless can_delete_result?(@result) + end + + def check_update_permissions + raise PermissionError.new(Result, :manage) unless can_manage_result?(@result) + end + + def permitted_includes + %w(comments result_texts tables assets) + end + + def result_params + raise TypeError unless params.require(:data).require(:type) == 'results' + + params.require(:data).require(:attributes).require(:name) + params.require(:data).permit(attributes: %i(name archived))[:attributes] + end + end + end +end diff --git a/app/serializers/api/v2/result_asset_serializer.rb b/app/serializers/api/v2/result_asset_serializer.rb new file mode 100644 index 000000000..728a2a979 --- /dev/null +++ b/app/serializers/api/v2/result_asset_serializer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultAssetSerializer < ActiveModel::Serializer + type :assets + attributes :file_id, :file_name, :file_size, :file_type, :file_url + + def file_id + object.asset&.id + end + + def file_name + object.asset&.file_name + end + + def file_size + object.asset&.file_size + end + + def file_type + object.asset&.content_type + end + + def file_url + if object.asset&.file&.attached? + Rails.application.routes.url_helpers.rails_blob_path(object.asset.file, disposition: 'attachment') + end + end + end + end +end diff --git a/app/serializers/api/v2/result_orderable_element_serializer.rb b/app/serializers/api/v2/result_orderable_element_serializer.rb new file mode 100644 index 000000000..7ef91f205 --- /dev/null +++ b/app/serializers/api/v2/result_orderable_element_serializer.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultOrderableElementSerializer < ActiveModel::Serializer + attributes :id, :position, :orderable, :orderable_type + + def orderable + case object.orderable_type + when 'ResultTable' + ResultTableSerializer.new(object.orderable, scope: { user: @instance_options[:user] }).as_json + when 'ResultText' + ResultTextSerializer.new(object.orderable, scope: { user: @instance_options[:user] }).as_json + end + end + end + end +end diff --git a/app/serializers/api/v2/result_serializer.rb b/app/serializers/api/v2/result_serializer.rb new file mode 100644 index 000000000..c88807d48 --- /dev/null +++ b/app/serializers/api/v2/result_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultSerializer < ActiveModel::Serializer + type :results + attributes :name, :archived + belongs_to :user, serializer: Api::V1::UserSerializer + + has_many :result_comments, key: :comments, serializer: Api::V1::CommentSerializer + has_many :result_texts, key: :result_texts, serializer: ResultTextSerializer + has_many :result_tables, key: :tables, serializer: ResultTableSerializer + has_many :result_assets, key: :assets, serializer: ResultAssetSerializer + has_many :result_orderable_elements, key: :result_elements, serializer: ResultOrderableElementSerializer + + include TimestampableModel + end + end +end diff --git a/app/serializers/api/v2/result_table_serializer.rb b/app/serializers/api/v2/result_table_serializer.rb new file mode 100644 index 000000000..f06bc5c46 --- /dev/null +++ b/app/serializers/api/v2/result_table_serializer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultTableSerializer < ActiveModel::Serializer + type :tables + attributes :table_id, :table_contents, :table_metadata + + def table_id + object.table&.id + end + + def table_contents + object.table&.contents&.force_encoding(Encoding::UTF_8) + end + + def table_metadata + object.table&.metadata + end + end + end +end diff --git a/app/serializers/api/v2/result_text_serializer.rb b/app/serializers/api/v2/result_text_serializer.rb new file mode 100644 index 000000000..3368ca59d --- /dev/null +++ b/app/serializers/api/v2/result_text_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Api + module V2 + class ResultTextSerializer < ActiveModel::Serializer + type :result_texts + attributes :name, :text + + def text + object.tinymce_render('text') + end + end + end +end diff --git a/config/initializers/api.rb b/config/initializers/api.rb index e49c585a8..29e4191b5 100644 --- a/config/initializers/api.rb +++ b/config/initializers/api.rb @@ -10,4 +10,6 @@ Rails.application.configure do config.x.core_api_rate_limit = ENV['CORE_API_RATE_LIMIT'] ? ENV['CORE_API_RATE_LIMIT'].to_i : 1000 config.x.core_api_v1_enabled = ENV['CORE_API_V1_ENABLED'] || false + + config.x.core_api_v2_enabled = ENV['CORE_API_V2_ENABLED'] || false end diff --git a/config/routes.rb b/config/routes.rb index 82f3d6640..161e34957 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,10 +7,6 @@ Rails.application.routes.draw do # Addons - def draw(routes_name) - instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb"))) - end - constraints UserSubdomain do devise_for :users, controllers: { registrations: 'users/registrations', sessions: 'users/sessions', @@ -953,6 +949,8 @@ Rails.application.routes.draw do end end end + + draw(:api_v2) if Rails.configuration.x.core_api_v2_enabled end end diff --git a/config/routes/api_v2.rb b/config/routes/api_v2.rb new file mode 100644 index 000000000..d97a8d3b3 --- /dev/null +++ b/config/routes/api_v2.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength + +namespace :v2, module: 'v1' do + resources :teams, only: %i(index show) do + resources :inventories, only: %i(index create show update destroy) do + resources :inventory_columns, + only: %i(index create show update destroy), + path: 'columns', + as: :columns do + resources :inventory_list_items, + only: %i(index create show update destroy), + path: 'list_items', + as: :list_items + resources :inventory_checklist_items, + only: %i(index create show update destroy), + path: 'checklist_items', + as: :checklist_items + resources :inventory_status_items, + only: %i(index create show update destroy), + path: 'status_items', + as: :status_items + resources :inventory_stock_unit_items, + only: %i(index create show update destroy), + path: 'stock_unit_items', + as: :stock_unit_items + end + resources :inventory_items, + only: %i(index create show update destroy), + path: '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 create update) do + resources :user_assignments, + only: %i(index show create update destroy), + controller: :project_user_assignments, + path: 'users', + as: :users + resources :project_comments, only: %i(index show), path: 'comments', as: :comments + get 'activities', to: 'projects#activities' + resources :reports, only: %i(index show), path: 'reports', as: :reports + resources :experiments, only: %i(index show create update) do + resources :user_assignments, + only: %i(index show update), + controller: :experiment_user_assignments + resources :task_groups, only: %i(index show) + resources :connections, only: %i(index show create destroy) + resources :tasks, only: %i(index show create update) do + resources :user_assignments, + only: %i(index show update), + controller: :task_user_assignments + resources :task_inventory_items, only: %i(index show update), + path: 'items', + as: :items + resources :task_users, only: %i(index show), + path: 'users', + as: :users + resources :task_tags, only: %i(index show), + path: 'tags', + as: :tags + resources :task_assignments, only: %i(index create destroy), + path: 'task_assignments', + as: :task_assignments + resources :protocols, only: %i(index show) do + resources :steps, only: %i(index show create update destroy) do + resources :assets, only: %i(index show create), path: 'attachments' + resources :checklists, only: %i(index show create update destroy), path: 'checklists' do + resources :checklist_items, + only: %i(index show create update destroy), + as: :items, + path: 'items' + end + resources :tables, only: %i(index show create update destroy), path: 'tables' + resources :step_texts, only: %i(index show create update destroy), path: 'step_texts' + end + end + get 'activities', to: 'tasks#activities' + end + end + end + resources :project_folders, only: %i(index show create update) + resources :users, only: %i(index) + resources :protocol_templates, only: %i(index show) + end + resources :users, only: %i(show) do + resources :user_identities, + only: %i(index create show update destroy), + path: 'identities', + as: :identities + end + + resources :user_roles, only: :index + resources :workflows, only: %i(index show) do + resources :workflow_statuses, path: :statuses, only: %i(index) + end +end + +namespace :v2 do + resources :teams do + resources :projects do + resources :experiments do + resources :tasks do + resources :results, only: %i(index create show update destroy) do + resources :result_assets, only: %i(index show create update destroy), path: 'assets' + resources :result_tables, only: %i(index show create update destroy), path: 'tables' + resources :result_texts, only: %i(index show create update destroy) + end + end + end + end + end +end + +# rubocop:enable Metrics/BlockLength diff --git a/spec/factories/result_orderable_elements.rb b/spec/factories/result_orderable_elements.rb new file mode 100644 index 000000000..20ae4dbdc --- /dev/null +++ b/spec/factories/result_orderable_elements.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :result_orderable_element do + result + sequence(:position) { |n| n } + + trait :result_text do + after(:build) do |result_orderable_element| + result_orderable_element.orderable ||= build(:result_text, result: result_orderable_element.result) + end + end + + trait :result_table do + after(:build) do |result_orderable_element| + result_orderable_element.orderable ||= build(:result_table, result: result_orderable_element.result) + end + end + end +end diff --git a/spec/factories/result_texts.rb b/spec/factories/result_texts.rb index 9c7c6747e..e2a3d5b2a 100644 --- a/spec/factories/result_texts.rb +++ b/spec/factories/result_texts.rb @@ -2,6 +2,7 @@ FactoryBot.define do factory :result_text do + name { Faker::Name.unique.name } text { Faker::Lorem.paragraph } result end diff --git a/spec/factories/step_reorderable_elements.rb b/spec/factories/step_reorderable_elements.rb index a9c1cfd09..20dc6e211 100644 --- a/spec/factories/step_reorderable_elements.rb +++ b/spec/factories/step_reorderable_elements.rb @@ -4,6 +4,6 @@ FactoryBot.define do factory :step_orderable_element do orderable { create :step_text, step: step } step - position { step ? step.step_orderable_elements.count : Faker::Number.between(from: 1, to: 10) } + sequence(:position) { |n| n } end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 02a01a03e..6eb78cd33 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -7,6 +7,7 @@ require_relative 'support/controller_macros' ENV['RAILS_ENV'] = 'test' ENV['CORE_API_V1_ENABLED'] = 'true' +ENV['CORE_API_V2_ENABLED'] = 'true' require File.expand_path('../../config/environment', __FILE__) # Prevent database truncation if the environment is production diff --git a/spec/requests/api/v2/result_assets_controller_spec.rb b/spec/requests/api/v2/result_assets_controller_spec.rb new file mode 100644 index 000000000..81a529af1 --- /dev/null +++ b/spec/requests/api/v2/result_assets_controller_spec.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength + +require 'rails_helper' + +RSpec.describe 'Api::V2::ResultAssetsController', type: :request do + let(:user) { create(:user) } + let(:team) { create(:team, created_by: user) } + let(:project) { create(:project, team: team, created_by: user) } + let(:experiment) { create(:experiment, :with_tasks, project: project, created_by: user) } + let(:task) { experiment.my_modules.first } + let(:result) { create(:result, user: user, my_module: task) } + let(:result_archived) { create(:result, :archived, user: user, my_module: task) } + let(:valid_headers) { { Authorization: "Bearer #{generate_token(user.id)}", 'Content-Type': 'application/json' } } + + let(:api_path) do + api_v2_team_project_experiment_task_result_result_assets_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id + ) + end + + describe 'GET result_assets, #index' do + let(:result_asset) { create(:result_asset, result: result) } + + context 'when has valid params' do + it 'renders 200' do + get api_path, headers: valid_headers + + expect(response).to have_http_status(200) + hash_body = nil + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match_array( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(result.result_assets, each_serializer: Api::V2::ResultAssetSerializer) + .to_json + )['data'] + ) + end + end + + context 'when result is not found' do + it 'renders 404' do + get api_v2_team_project_experiment_task_result_result_assets_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: -1 + ), headers: valid_headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET result_asset, #show' do + let(:result_asset) { create(:result_asset, result: result) } + + context 'when has valid params' do + it 'renders 200' do + hash_body = nil + get api_v2_team_project_experiment_task_result_result_asset_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_asset.asset.id + ), headers: valid_headers + + expect(response).to have_http_status(200) + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(result_asset, serializer: Api::V2::ResultAssetSerializer) + .to_json + )['data'] + ) + end + end + end + + describe 'POST result_asset, #create' do + let(:action) do + post(api_path, params: request_body.to_json, headers: valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'attachments', + attributes: { + file_data: "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAE0lEQVQIHWP8//8/ + AwMDExADAQAkBgMBOOSShwAAAABJRU5ErkJggg=='\''", + file_type: 'image/jpeg', + file_name: 'test.jpg' + } + } + } + end + + it 'creates new result_asset' do + expect { action }.to change { ResultAsset.count }.by(1) + end + + it 'returns status 201' do + action + + expect(response).to have_http_status(201) + end + + it 'returns well-formatted response' do + hash_body = nil + action + + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(ResultAsset.last, serializer: Api::V2::ResultAssetSerializer) + .to_json + )['data'] + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'attachments', + attributes: {} + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'DELETE result_asset, #destroy' do + let(:result_asset) { create(:result_asset, result: result) } + let(:result_asset_archived) { create(:result_asset, result: result_archived) } + + let(:action) do + delete(api_v2_team_project_experiment_task_result_result_asset_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_asset.asset.id + ), headers: valid_headers) + end + + let(:action_archived) do + delete(api_v2_team_project_experiment_task_result_result_asset_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result_archived.id, + id: result_asset_archived.asset.id + ), headers: valid_headers) + end + + it 'deletes result_asset' do + action + expect(response).to have_http_status(200) + expect(ResultAsset.where(id: result_asset.id)).to_not exist + expect(Asset.where(id: result_asset.asset.id)).to_not exist + end + + it 'does not delete result_asset of archived result' do + action_archived + expect(response).to have_http_status(403) + expect(ResultAsset.where(id: result_asset_archived.id)).to exist + expect(Asset.where(id: result_asset_archived.asset.id)).to exist + end + end +end + +# rubocop:enable Metrics/BlockLength diff --git a/spec/requests/api/v2/result_tables_controller_spec.rb b/spec/requests/api/v2/result_tables_controller_spec.rb new file mode 100644 index 000000000..2402a7fc7 --- /dev/null +++ b/spec/requests/api/v2/result_tables_controller_spec.rb @@ -0,0 +1,289 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength + +require 'rails_helper' + +RSpec.describe 'Api::V2::ResultTablesController', type: :request do + let(:user) { create(:user) } + let(:team) { create(:team, created_by: user) } + let(:project) { create(:project, team: team, created_by: user) } + let(:experiment) { create(:experiment, :with_tasks, project: project, created_by: user) } + let(:task) { experiment.my_modules.first } + let(:result) { create(:result, user: user, my_module: task) } + let(:result_archived) { create(:result, :archived, user: user, my_module: task) } + let(:valid_headers) { { Authorization: "Bearer #{generate_token(user.id)}", 'Content-Type': 'application/json' } } + + let(:api_path) do + api_v2_team_project_experiment_task_result_result_tables_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id + ) + end + + describe 'GET result_tables, #index' do + let!(:result_orderable_element) { create(:result_orderable_element, :result_table, result: result) } + + context 'when has valid params' do + it 'renders 200' do + get api_path, headers: valid_headers + + expect(response).to have_http_status(200) + hash_body = nil + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match_array( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(result.result_tables, each_serializer: Api::V2::ResultTableSerializer) + .to_json + )['data'] + ) + end + end + + context 'when result is not found' do + it 'renders 404' do + get api_v2_team_project_experiment_task_result_result_tables_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: -1 + ), headers: valid_headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET result_table, #show' do + let(:result_table) { create(:result_table, result: result) } + + context 'when has valid params' do + it 'renders 200' do + hash_body = nil + get api_v2_team_project_experiment_task_result_result_table_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_table.table.id + ), headers: valid_headers + + expect(response).to have_http_status(200) + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(result_table, serializer: Api::V2::ResultTableSerializer) + .to_json + )['data'] + ) + end + end + end + + describe 'POST result_table, #create' do + let(:action) do + post(api_path, params: request_body.to_json, headers: valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'tables', + attributes: { + name: 'Result table', + contents: '{"data": [["group/time", "1 dpi", "6 dpi", "", ""], ["PVYNTN", "1", "1", "", ""]]}' + } + } + } + end + + it 'creates new result_table' do + expect { action }.to change { ResultTable.count }.by(1) + end + + it 'returns status 201' do + action + + expect(response).to have_http_status 201 + end + + it 'returns well-formatted response' do + hash_body = nil + action + + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(ResultTable.last, serializer: Api::V2::ResultTableSerializer) + .to_json + )['data'] + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'result_tables', + attributes: {} + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'PATCH result_table, #update' do + let(:result_table) { create(:result_table, result: result) } + let(:result_table_archived) { create(:result_table, result: result_archived) } + let(:action) do + patch(api_v2_team_project_experiment_task_result_result_table_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_table.table.id + ), params: request_body.to_json, headers: valid_headers) + end + + let(:action_archived) do + patch(api_v2_team_project_experiment_task_result_result_table_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result_archived.id, + id: result_table_archived.table.id + ), params: request_body.to_json, headers: valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'tables', + attributes: { + name: 'Result table', + contents: '{"data": [["group/time", "1 dpi", "6 dpi", "", ""], ["PVYNTN", "1", "1", "", ""]]}' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well-formatted response' do + hash_body = nil + action + + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(ResultTable.last, serializer: Api::V2::ResultTableSerializer) + .to_json + )['data'] + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'result_tables', + attributes: {} + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + + context 'when result is archived' do + let(:request_body) do + { + data: { + type: 'result_tables', + attributes: { + name: 'Result table', + contents: '{"data": [["group/time", "1 dpi", "6 dpi", "", ""], ["PVYNTN", "1", "1", "", ""]]}' + } + } + } + end + + it 'renders 403' do + action_archived + + expect(response).to have_http_status(403) + end + end + end + + describe 'DELETE result_table, #destroy' do + let(:result_table) { create(:result_table, result: result) } + let(:result_table_archived) { create(:result_table, result: result_archived) } + let(:delete_action) do + delete(api_v2_team_project_experiment_task_result_result_table_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_table.table.id + ), headers: valid_headers) + end + + let(:delete_action_archived) do + delete(api_v2_team_project_experiment_task_result_result_table_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result_archived.id, + id: result_table_archived.table.id + ), headers: valid_headers) + end + + it 'deletes result_table' do + delete_action + expect(response).to have_http_status(200) + expect(ResultTable.where(id: result_table.id)).to_not exist + expect(Table.where(id: result_table.table.id)).to_not exist + end + + it 'does not delete result_table of archived result' do + delete_action_archived + expect(response).to have_http_status(403) + expect(ResultTable.where(id: result_table_archived.id)).to exist + expect(Table.where(id: result_table_archived.table.id)).to exist + end + end +end + +# rubocop:enable Metrics/BlockLength diff --git a/spec/requests/api/v2/result_texts_controller_spec.rb b/spec/requests/api/v2/result_texts_controller_spec.rb new file mode 100644 index 000000000..87a2f27f0 --- /dev/null +++ b/spec/requests/api/v2/result_texts_controller_spec.rb @@ -0,0 +1,373 @@ +# frozen_string_literal: true +# rubocop:disable Metrics/BlockLength + +require 'rails_helper' + +RSpec.describe 'Api::V2::ResultTextsController', type: :request do + let(:user) { create(:user) } + let(:team) { create(:team, created_by: user) } + let(:project) { create(:project, team: team, created_by: user) } + let(:experiment) { create(:experiment, :with_tasks, project: project, created_by: user) } + let(:task) { experiment.my_modules.first } + let(:result) { create(:result, user: user, my_module: task) } + let(:result_archived) { create(:result, :archived, user: user, my_module: task) } + let(:valid_headers) { { Authorization: "Bearer #{generate_token(user.id)}", 'Content-Type': 'application/json' } } + + let(:api_path) do + api_v2_team_project_experiment_task_result_result_texts_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id + ) + end + + describe 'GET result_texts, #index' do + let!(:result_orderable_element) { create(:result_orderable_element, :result_text, result: result) } + + context 'when has valid params' do + it 'renders 200' do + get api_path, headers: valid_headers + + expect(response).to have_http_status(200) + hash_body = nil + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match_array( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(result.result_texts, each_serializer: Api::V2::ResultTextSerializer) + .to_json + )['data'] + ) + end + end + + context 'when result is not found' do + it 'renders 404' do + get api_v2_team_project_experiment_task_result_result_texts_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: -1 + ), headers: valid_headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET result_text, #show' do + let(:result_text) { create(:result_text, result: result) } + + context 'when has valid params' do + it 'renders 200' do + hash_body = nil + get api_v2_team_project_experiment_task_result_result_text_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_text.id + ), headers: valid_headers + + expect(response).to have_http_status(200) + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(result_text, serializer: Api::V2::ResultTextSerializer) + .to_json + )['data'] + ) + end + end + end + + describe 'POST result_text, #create' do + let(:action) do + post(api_path, params: request_body.to_json, headers: valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'result_texts', + attributes: { + name: 'Result text', + text: '

Hello!

' + } + } + } + end + + it 'creates new result_text' do + expect { action }.to change { ResultText.count }.by(1) + end + + it 'returns status 201' do + action + + expect(response).to have_http_status(201) + end + + it 'returns well-formatted response' do + hash_body = nil + action + + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(ResultText.last, serializer: Api::V2::ResultTextSerializer) + .to_json + )['data'] + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'result_texts', + attributes: {} + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + + context 'when include tinymce' do + let(:request_body) do + { + data: { + type: 'result_texts', + attributes: { + name: 'Result text', + text: "Result text 1 " + } + }, + included: [ + { + type: 'tiny_mce_assets', + attributes: { + file_data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAA'\ + 'AACCAIAAAD91JpzAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAE0lE'\ + 'QVQIHWP8//8/AwMDExADAQAkBgMBOOSShwAAAABJRU5ErkJggg==', + file_token: '1', + file_name: 'test.png' + } + } + ] + } + end + + it 'Response with correct text result and TinyMCE images' do + hash_body = nil + action + + expect(response).to have_http_status 201 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(ResultText.last, serializer: Api::V2::ResultTextSerializer) + .to_json + )['data'] + ) + expect(ResultText.last.text).to include "data-mce-token=\"#{Base62.encode(TinyMceAsset.last.id)}\"" + end + end + end + + describe 'PATCH result_text, #update' do + let(:result_text) { create(:result_text, result: result) } + let(:result_text_archived) { create(:result_text, result: result_archived) } + let(:action) do + patch(api_v2_team_project_experiment_task_result_result_text_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_text.id + ), params: request_body.to_json, headers: valid_headers) + end + let(:action_archived) do + patch(api_v2_team_project_experiment_task_result_result_text_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result_archived.id, + id: result_text_archived.id + ), params: request_body.to_json, headers: valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'result_texts', + attributes: { + name: 'Result text', + text: '

Hello!

' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well-formatted response' do + hash_body = nil + action + + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(ResultText.last, serializer: Api::V2::ResultTextSerializer) + .to_json + )['data'] + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'result_texts', + attributes: {} + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + + context 'when result is archived' do + let(:request_body) do + { + data: { + type: 'result_texts', + attributes: { + name: 'Result text', + text: '

Hello!

' + } + } + } + end + + it 'renders 403' do + action_archived + + expect(response).to have_http_status(403) + end + end + + context 'when include tinymce' do + let(:request_body) do + { + data: { + type: 'result_texts', + attributes: { + name: 'Result text', + text: 'Result text 1 ' + } + }, + included: [ + { + type: 'tiny_mce_assets', + attributes: { + file_data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAA'\ + 'AACCAIAAAD91JpzAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAE0lE'\ + 'QVQIHWP8//8/AwMDExADAQAkBgMBOOSShwAAAABJRU5ErkJggg==', + file_token: '1', + file_name: 'test.png' + } + } + ] + } + end + + it 'Response with correct text result and TinyMCE images' do + hash_body = nil + action + + expect(response).to have_http_status 200 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(ResultText.last, serializer: Api::V2::ResultTextSerializer) + .to_json + )['data'] + ) + expect(ResultText.last.text).to include "data-mce-token=\"#{Base62.encode(TinyMceAsset.last.id)}\"" + end + end + end + + describe 'DELETE result_text, #destroy' do + let(:result_text) { create(:result_text, result: result) } + let(:result_text_archived) { create(:result_text, result: result_archived) } + let(:action) do + delete(api_v2_team_project_experiment_task_result_result_text_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result.id, + id: result_text.id + ), headers: valid_headers) + end + + let(:action_archived) do + delete(api_v2_team_project_experiment_task_result_result_text_path( + team_id: team.id, + project_id: project.id, + experiment_id: experiment.id, + task_id: task.id, + result_id: result_archived.id, + id: result_text_archived.id + ), headers: valid_headers) + end + + it 'deletes result_text' do + action + expect(response).to have_http_status(200) + expect(ResultText.where(id: result_text.id)).to_not exist + end + + it 'does not delete result_text of archived result' do + action_archived + expect(response).to have_http_status(403) + expect(ResultText.where(id: result_text_archived.id)).to exist + end + end +end + +# rubocop:enable Metrics/BlockLength diff --git a/spec/requests/api/v2/results_controller_spec.rb b/spec/requests/api/v2/results_controller_spec.rb new file mode 100644 index 000000000..a31bc1e3d --- /dev/null +++ b/spec/requests/api/v2/results_controller_spec.rb @@ -0,0 +1,369 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength + +require 'rails_helper' + +RSpec.describe 'Api::V2::ResultsController', type: :request do + let(:user) { create(:user) } + let(:another_user) { create(:user) } + let(:team1) { create(:team, created_by: user) } + let(:team2) { create(:team, created_by: another_user) } + + let(:valid_project) { create(:project, name: Faker::Name.unique.name, created_by: user, team: team1) } + let(:unaccessible_project) { create(:project, name: Faker::Name.unique.name, created_by: user, team: team2) } + + let(:valid_experiment) { create(:experiment, created_by: user, last_modified_by: user, project: valid_project) } + let(:unaccessible_experiment) { create(:experiment, created_by: user, last_modified_by: user, project: unaccessible_project) } + + let(:valid_task) { create(:my_module, created_by: user, last_modified_by: user, experiment: valid_experiment) } + let(:unaccessible_task) { create(:my_module, created_by: user, last_modified_by: user, experiment: unaccessible_experiment) } + + let!(:results) { create_list(:result, 3, user: user, last_modified_by: user, my_module: valid_task) } + let!(:unaccessible_result) { create(:result, user: user, last_modified_by: user, my_module: unaccessible_task) } + + let(:valid_hash_body) do + { + data: { + type: 'results', + attributes: { + name: Faker::Name.unique.name + } + } + } + end + + let(:valid_headers) do + { + Authorization: "Bearer #{generate_token(user.id)}", + 'Content-Type': 'application/json' + } + end + + describe 'GET results, #index' do + let(:api_path) do + api_v2_team_project_experiment_task_results_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task + ) + end + + it 'Response with correct results' do + hash_body = nil + get api_path, headers: valid_headers + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(valid_task.results, each_serializer: Api::V2::ResultSerializer) + .to_json + )['data'] + ) + end + + it 'When invalid request, task from another experiment' do + hash_body = nil + get api_v2_team_project_experiment_task_results_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: unaccessible_task + ), headers: valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 404) + end + + it 'When invalid request, user is not a member of the team' do + hash_body = nil + get api_v2_team_project_experiment_task_results_path( + team_id: team2.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task + ), headers: valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 403) + end + + it 'When invalid request, non-existing task' do + hash_body = nil + get api_v2_team_project_experiment_task_results_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: -1 + ), headers: valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 404) + end + end + + describe 'GET result, #show' do + let(:result_valid) { valid_task.results.first } + let(:api_path) do + api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: result_valid.id + ) + end + + it 'When valid request, user can read result' do + hash_body = nil + get api_path, headers: valid_headers + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(result_valid, serializer: Api::V2::ResultSerializer) + .to_json + )['data'] + ) + end + + it 'When invalid request, user is not a member of the team' do + hash_body = nil + get api_v2_team_project_experiment_task_result_path( + team_id: team2.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: result_valid.id + ), headers: valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 403) + end + + it 'When invalid request, non-existing result' do + hash_body = nil + get api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: -1 + ), headers: valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 404) + end + + it 'When invalid request, result from an unaccessible task' do + hash_body = nil + get api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: unaccessible_task, + id: unaccessible_result.id + ), headers: valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 404) + end + end + + describe 'POST result, #create' do + let(:api_path) do + api_v2_team_project_experiment_task_results_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task + ) + end + + it 'Response with correct result' do + hash_body = nil + post api_path, params: valid_hash_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( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(Result.last, serializer: Api::V2::ResultSerializer) + .to_json + )['data'] + ) + end + + it 'When invalid request, non-existing task' do + hash_body = nil + post api_v2_team_project_experiment_task_results_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: -1 + ), params: valid_hash_body.to_json, headers: valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 404) + end + + it 'When invalid request, user is not a member of the team' do + hash_body = nil + post api_v2_team_project_experiment_task_results_path( + team_id: team2.id, + project_id: unaccessible_project, + experiment_id: unaccessible_experiment, + task_id: unaccessible_task + ), params: valid_hash_body.to_json, headers: valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 403) + end + + it 'When invalid request, task from another experiment' do + hash_body = nil + post api_v2_team_project_experiment_task_results_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: unaccessible_task + ), params: valid_hash_body.to_json, headers: valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include(status: 404) + end + end + + describe 'PUT result, #update' do + let(:result_valid) { valid_task.results.first } + let(:request_body) do + { + data: { + type: 'results', + attributes: { + name: 'my result' + } + } + } + end + + let(:action) do + put(api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: result_valid.id + ), params: request_body.to_json, headers: valid_headers) + end + + context 'when has attributes for update' do + it 'updates tasks name' do + action + expect(result_valid.reload.name).to eq('my result') + end + + it 'returns status 200' do + action + expect(response).to have_http_status 200 + end + end + + context 'when there is nothing to update' do + let(:request_body_with_same_name) do + { + data: { + type: 'results', + attributes: { + name: result_valid.reload.name + } + } + } + end + + it 'returns 204' do + put(api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: result_valid.id + ), params: request_body_with_same_name.to_json, headers: valid_headers) + + expect(response).to have_http_status 204 + end + end + + context 'when result is archived' do + let(:result_archived) do + create(:result, :archived, user: user, last_modified_by: user, my_module: valid_task) + end + + let(:request_with_archived_result) do + { + data: { + type: 'results', + attributes: { + name: 'Result archived' + } + } + } + end + + it 'returns 403' do + put(api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: result_archived.id + ), params: request_with_archived_result.to_json, headers: valid_headers) + + expect(response).to have_http_status 403 + end + end + end + + describe 'DELETE result, #destroy' do + let(:result_archived) do + create(:result, :archived, user: user, last_modified_by: user, my_module: valid_task) + end + + let(:result_valid) { valid_task.results.first } + + let(:action) do + delete(api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: result_valid.id + ), headers: valid_headers) + end + + let(:action_archived) do + delete(api_v2_team_project_experiment_task_result_path( + team_id: team1.id, + project_id: valid_project, + experiment_id: valid_experiment, + task_id: valid_task, + id: result_archived.id + ), headers: valid_headers) + end + + it 'deletes result' do + action_archived + expect(response).to have_http_status(200) + expect(Result.where(id: result_archived.id)).to_not exist + end + + it 'does not delete archived result' do + action + expect(response).to have_http_status(403) + expect(Result.where(id: result_valid.id)).to exist + end + end +end + +# rubocop:enable Metrics/BlockLength