diff --git a/app/controllers/workflow_executions_controller.rb b/app/controllers/workflow_executions_controller.rb index 83362f1914..a173d410d6 100644 --- a/app/controllers/workflow_executions_controller.rb +++ b/app/controllers/workflow_executions_controller.rb @@ -54,7 +54,7 @@ def workflow_execution_params_attributes def samples_workflow_execution_params_attributes [ - :id, + :id, # index, increment for each one, not necissary for functionality :sample_id, { samplesheet_params: {} } ] diff --git a/app/graphql/mutations/submit_workflow_execution.rb b/app/graphql/mutations/submit_workflow_execution.rb new file mode 100644 index 0000000000..25743a579c --- /dev/null +++ b/app/graphql/mutations/submit_workflow_execution.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module Mutations + # Base Mutation + class SubmitWorkflowExecution < BaseMutation # rubocop:disable Metrics/ClassLength + null true + description 'Create a new workflow execution..' + + argument :email_notification, + Boolean, + required: false, + default_value: false, + description: 'Set to true to enable email notifications from this workflow execution' + argument :name, String, required: false, description: 'Name for the new workflow.' + argument :samples_workflow_executions_attributes, [GraphQL::Types::JSON], description: "A list of hashes containing a 'sample_id', and a hash of `samplesheet_params`." # rubocop:disable GraphQL/ExtractInputType,Layout/LineLength + argument :update_samples, # rubocop:disable GraphQL/ExtractInputType + Boolean, + required: false, + default_value: false, + description: 'Set true for samples to be updated from this workflow execution' + argument :workflow_engine, String, description: '' # rubocop:disable GraphQL/ExtractInputType + argument :workflow_engine_parameters, [GraphQL::Types::JSON], description: 'List of Hashes containing `key` and `value` to be passed to the workflow engine.' # rubocop:disable GraphQL/ExtractInputType,Layout/LineLength + argument :workflow_engine_version, String, description: 'Workflow Engine Version' # rubocop:disable GraphQL/ExtractInputType + argument :workflow_name, String, description: 'Name of the pipeline to be run on this workflow execution' # rubocop:disable GraphQL/ExtractInputType + argument :workflow_params, GraphQL::Types::JSON, description: 'Parameters to be passed to the pipeline.' # rubocop:disable GraphQL/ExtractInputType + argument :workflow_type, String, description: 'Type of pipelines workflow.' # rubocop:disable GraphQL/ExtractInputType + argument :workflow_type_version, String, description: 'Version of the pipelines workflow type.' # rubocop:disable GraphQL/ExtractInputType + argument :workflow_url, String, description: 'Url for the pipeline.' # rubocop:disable GraphQL/ExtractInputType + argument :workflow_version, String, description: 'Version of the pipeline to be run on this workflow execution' # rubocop:disable GraphQL/ExtractInputType + + # one of project/group, to use as the namespace + argument :project_id, ID, # rubocop:disable GraphQL/ExtractInputType + required: false, + description: 'The Node ID of the project to run workflow in. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`.' # rubocop:disable Layout/LineLength + argument :group_id, ID, # rubocop:disable GraphQL/OrderedArguments,GraphQL/ExtractInputType + required: false, + description: 'The Node ID of the group to run workflow in. For example, `gid://irida/Group/a84cd757-dedb-4c64-8b01-097020163077`.' # rubocop:disable Layout/LineLength + validates required: { one_of: %i[project_id group_id] } + + field :errors, [Types::UserErrorType], null: false, description: 'A list of errors that prevented the mutation.' + field :workflow_execution, Types::WorkflowExecutionType, description: 'The newly created workflow execution.' + + def resolve(args) + create_workflow_execution(args) + end + + def ready?(**_args) + authorize!(to: :mutate?, with: GraphqlPolicy, context: { user: context[:current_user], token: context[:token] }) + end + + private + + def create_workflow_execution(args) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize + workflow_engine_parameters = build_workflow_engine_parameters(args[:workflow_engine_parameters]) + + update_samples = args[:update_samples] ? '1' : '0' + email_notification = args[:email_notification] ? '1' : '0' + + namespace_id = namespace(args[:project_id], args[:group_id]) + + samples_workflow_executions_attributes = build_samples_workflow_executions_attributes(args[:samples_workflow_executions_attributes]) # rubocop:disable Layout/LineLength + + workflow_execution_params = { + name: args[:name], + metadata: { workflow_name: args[:workflow_name], + workflow_version: args[:workflow_version] }, + namespace_id:, + workflow_params: args[:workflow_params], + workflow_type: args[:workflow_type], + workflow_type_version: args[:workflow_type_version], + workflow_engine: args[:workflow_engine], + workflow_engine_version: args[:workflow_engine_version], + workflow_engine_parameters:, + workflow_url: args[:workflow_url], + update_samples:, + email_notification:, + samples_workflow_executions_attributes: + } + + workflow_execution = WorkflowExecutions::CreateService.new( + current_user, workflow_execution_params + ).execute + + if workflow_execution.persisted? + { + workflow_execution:, + errors: [] + } + else + user_errors = workflow_execution.errors.map do |error| + { + path: ['workflow_execution', error.attribute.to_s.camelize(:lower)], + message: error.message + } + end + { + workflow_execution: nil, + errors: user_errors + } + end + end + + def namespace(project_id, group_id) + if project_id + IridaSchema.object_from_id(project_id, { expected_type: Project }).namespace.id + else # group_id + IridaSchema.object_from_id(group_id, { expected_type: Group }).id + end + end + + # workflow engine parameters can have keys that start with `-` which is not allowed in graphql, + # so we parse a list of key value pairs into a hash that can be used. + def build_workflow_engine_parameters(parameter_list) + result = {} + parameter_list.each do |params| + result[params['key']] = params['value'] + end + result + end + + def build_samples_workflow_executions_attributes(samples_workflow_executions_attributes) + result = {} + samples_workflow_executions_attributes.each_with_index do |data, index| + sample_id = IridaSchema.object_from_id(data['sample_id'], { expected_type: Sample }).id + result[index.to_s] = { + 'sample_id' => sample_id, + 'samplesheet_params' => data['samplesheet_params'] + } + end + + result + end + end +end diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 360d4fdb42..86e4026a0e 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -1416,6 +1416,16 @@ type Mutation { input: CreateSampleInput! ): CreateSamplePayload + """ + Create a new workflow execution.. + """ + submitWorkflowExecution( + """ + Parameters for SubmitWorkflowExecution + """ + input: SubmitWorkflowExecutionInput! + ): SubmitWorkflowExecutionPayload + """ Transfer a list of sample to another project. """ @@ -2857,6 +2867,111 @@ type SamplesWorkflowExecution implements Node { workflowExecution: WorkflowExecution } +""" +Autogenerated input type of SubmitWorkflowExecution +""" +input SubmitWorkflowExecutionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Set to true to enable email notifications from this workflow execution + """ + emailNotification: Boolean = false + + """ + The Node ID of the group to run workflow in. For example, `gid://irida/Group/a84cd757-dedb-4c64-8b01-097020163077`. + """ + groupId: ID + + """ + Name for the new workflow. + """ + name: String + + """ + The Node ID of the project to run workflow in. For example, `gid://irida/Project/a84cd757-dedb-4c64-8b01-097020163077`. + """ + projectId: ID + + """ + A list of hashes containing a 'sample_id', and a hash of `samplesheet_params`. + """ + samplesWorkflowExecutionsAttributes: [JSON!]! + + """ + Set true for samples to be updated from this workflow execution + """ + updateSamples: Boolean = false + + """ + + """ + workflowEngine: String! + + """ + List of Hashes containing `key` and `value` to be passed to the workflow engine. + """ + workflowEngineParameters: [JSON!]! + + """ + Workflow Engine Version + """ + workflowEngineVersion: String! + + """ + Name of the pipeline to be run on this workflow execution + """ + workflowName: String! + + """ + Parameters to be passed to the pipeline. + """ + workflowParams: JSON! + + """ + Type of pipelines workflow. + """ + workflowType: String! + + """ + Version of the pipelines workflow type. + """ + workflowTypeVersion: String! + + """ + Url for the pipeline. + """ + workflowUrl: String! + + """ + Version of the pipeline to be run on this workflow execution + """ + workflowVersion: String! +} + +""" +Autogenerated return type of SubmitWorkflowExecution. +""" +type SubmitWorkflowExecutionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A list of errors that prevented the mutation. + """ + errors: [UserError!]! + + """ + The newly created workflow execution. + """ + workflowExecution: WorkflowExecution +} + """ Autogenerated input type of TransferSamples """ diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index ba119d5073..498873821f 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -13,6 +13,7 @@ class MutationType < Types::BaseObject field :create_group, mutation: Mutations::CreateGroup # rubocop:disable GraphQL/FieldDescription field :create_project, mutation: Mutations::CreateProject # rubocop:disable GraphQL/FieldDescription field :create_sample, mutation: Mutations::CreateSample # rubocop:disable GraphQL/FieldDescription,GraphQL/ExtractType + field :submit_workflow_execution, mutation: Mutations::SubmitWorkflowExecution # rubocop:disable GraphQL/FieldDescription field :transfer_samples, mutation: Mutations::TransferSamples # rubocop:disable GraphQL/FieldDescription field :update_sample_metadata, mutation: Mutations::UpdateSampleMetadata # rubocop:disable GraphQL/FieldDescription end diff --git a/app/models/samples_workflow_execution.rb b/app/models/samples_workflow_execution.rb index 7040e5fdcf..082413262e 100644 --- a/app/models/samples_workflow_execution.rb +++ b/app/models/samples_workflow_execution.rb @@ -9,4 +9,6 @@ class SamplesWorkflowExecution < ApplicationRecord belongs_to :sample has_many_attached :inputs has_many :outputs, dependent: :destroy, class_name: 'Attachment', as: :attachable + + validates_with WorkflowExecutionSamplesheetParamsValidator end diff --git a/app/models/workflow_execution.rb b/app/models/workflow_execution.rb index 54102bd15e..c3663fdcee 100644 --- a/app/models/workflow_execution.rb +++ b/app/models/workflow_execution.rb @@ -40,7 +40,7 @@ def send_email end def cancellable? - %w[submitted running prepared initial].include?(state) + %w[submitted running prepared initial].include?(state) end def deletable? diff --git a/app/validators/workflow_execution_samplesheet_params_validator.rb b/app/validators/workflow_execution_samplesheet_params_validator.rb new file mode 100644 index 0000000000..0829a0197c --- /dev/null +++ b/app/validators/workflow_execution_samplesheet_params_validator.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Validator for Workflow Execution Samplesheet Params +# This will cause the validation to fail if any of the attachment ids cannot be resolved +class WorkflowExecutionSamplesheetParamsValidator < ActiveModel::Validator + def validate(record) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + record.samplesheet_params.each do |key, value| # rubocop:disable Metrics/BlockLength + if key == 'sample' + if value.nil? || value == '' + error_message = 'No Sample PUID provided' + record.errors.add :sample, error_message + record.workflow_execution.errors.add :sample, error_message + elsif value != (record.sample.puid) + error_message = "Provided Sample PUID #{value} does not match SampleWorkflowExecution Sample PUID #{record.sample.puid}" # rubocop:disable Layout/LineLength + record.errors.add :sample, error_message + record.workflow_execution.errors.add :sample, error_message + end + next + end + + next if value == '' + + begin + # Attempt to parse an object from the id provided + attachment = IridaSchema.object_from_id(value, { expected_type: Attachment }) + unless attachment.attachable == record.sample + error_message = "Attachment does not belong to Sample #{record.sample.puid}." + record.errors.add :attachment, error_message + record.workflow_execution.errors.add :attachment, error_message + end + rescue StandardError => e + error_message = e.message + record.errors.add :attachment, error_message + record.workflow_execution.errors.add :attachment, error_message + next + end + end + end +end diff --git a/db/seeds.rb b/db/seeds.rb index f549d04149..f2483ddd3e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -166,7 +166,15 @@ def seed_workflow_executions # rubocop:disable Metrics/MethodLength, Metrics/Abc workflow_engine_version: '23.10.0', workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexample', - submitter: User.find_by(email: 'user1@email.com') + submitter: User.find_by(email: 'user1@email.com'), + samples_workflow_executions_attributes: { + '0': { + sample_id: Sample.first.id, + samplesheet_params: { + sample: Sample.first.puid + } + } + } ) SamplesWorkflowExecution.create( @@ -188,7 +196,15 @@ def seed_workflow_executions # rubocop:disable Metrics/MethodLength, Metrics/Abc workflow_url: 'https://github.com/phac-nml/iridanextexample', submitter: User.find_by(email: 'user1@email.com'), blob_run_directory: 'this should be a generated key', - state: :completed + state: :completed, + samples_workflow_executions_attributes: { + '0': { + sample_id: Sample.first.id, + samplesheet_params: { + sample: Sample.first.puid + } + } + } ) filename = 'summary.txt' @@ -197,7 +213,6 @@ def seed_workflow_executions # rubocop:disable Metrics/MethodLength, Metrics/Abc attachment.save! SamplesWorkflowExecution.create( - samplesheet_params: { my_key1: 'my_value_2', my_key2: 'my_value_3' }, sample: Sample.first, workflow_execution: workflow_execution_completed ) diff --git a/test/controllers/workflow_executions_controller_test.rb b/test/controllers/workflow_executions_controller_test.rb index 17388440b7..4c00eb9eee 100644 --- a/test/controllers/workflow_executions_controller_test.rb +++ b/test/controllers/workflow_executions_controller_test.rb @@ -30,7 +30,7 @@ class WorkflowExecutionsControllerTest < ActionDispatch::IntegrationTest { sample_id: @sample1.id, samplesheet_params: { - sample: "Sample_#{@sample1.id}", + sample: @sample1.puid, 'fastq_1' => @attachment1.to_global_id, 'fastq_2' => '' } diff --git a/test/fixtures/samples_workflow_executions.yml b/test/fixtures/samples_workflow_executions.yml index 7fe93badfa..fa0059b5eb 100644 --- a/test/fixtures/samples_workflow_executions.yml +++ b/test/fixtures/samples_workflow_executions.yml @@ -5,23 +5,44 @@ samples_workflow_executions_valid: workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:workflow_execution_valid, :uuid) %> samplesheet_params: { - key_c: 'value_c' + "sample": INXT_SAM_AAAAAAAAAA, + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %> } -samples_workflow_executions_invalid_no_sample: - sample_id: null +samples_workflow_executions_invalid_no_sample_puid: + sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> + workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:workflow_execution_valid, :uuid) %> + samplesheet_params: + { + "sample": '', + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %> + } + +samples_workflow_executions_invalid_mismatch_sample_puid: + sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> + workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:workflow_execution_valid, :uuid) %> + samplesheet_params: + { + "sample": INXT_SAM_AAAAAAAAAB, + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %> + } + +samples_workflow_executions_invalid_file_id: + sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:workflow_execution_valid, :uuid) %> samplesheet_params: { - key_c: 'value_c' + "sample": INXT_SAM_AAAAAAAAAA, + "fastq_1": 12345 } -samples_workflow_executions_invalid_no_workflow_execution: +samples_workflow_executions_mismatch_file_id: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> - workflow_execution_id: null + workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:workflow_execution_valid, :uuid) %> samplesheet_params: { - key_c: 'value_c' + "sample": INXT_SAM_AAAAAAAAAA, + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachmentA, :uuid)}" %> } sample1_irida_next_example: @@ -117,92 +138,42 @@ sample1_irida_next_example_new: sample41_irida_next_example_completing_c: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample41, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_c, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAABQ, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %>, - "fastq_2": "" - } sample42_irida_next_example_completing_c: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample42, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_c, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAABR, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment2, :uuid)}" %>, - "fastq_2": "" - } sample41_irida_next_example_completing_d: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample41, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_d, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAABQ, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %>, - "fastq_2": "" - } sample42_irida_next_example_completing_d: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample42, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_d, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAABR, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment2, :uuid)}" %>, - "fastq_2": "" - } sample41_irida_next_example_completing_e: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample41, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_e, :uuid) %> - samplesheet_params: { - "sample": <%= "Sample_#{ActiveRecord::FixtureSet.identify(:sample41, :uuid)}" %>, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %>, - "fastq_2": "" - } sample42_irida_next_example_completing_e: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample42, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_e, :uuid) %> - samplesheet_params: { - "sample": <%= "Sample_#{ActiveRecord::FixtureSet.identify(:sample42, :uuid)}" %>, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment2, :uuid)}" %>, - "fastq_2": "" - } sample41_irida_next_example_completing_f: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample41, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_f, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAABQ, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %>, - "fastq_2": "" - } sample42_irida_next_example_completing_f: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample42, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_f, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAABR, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment2, :uuid)}" %>, - "fastq_2": "" - } sampleA_irida_next_example_completing_g: sample_id: <%= ActiveRecord::FixtureSet.identify(:sampleA, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_g, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAAA3, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %>, - "fastq_2": "" - } sampleB_irida_next_example_completing_g: sample_id: <%= ActiveRecord::FixtureSet.identify(:sampleB, :uuid) %> workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_g, :uuid) %> - samplesheet_params: { - "sample": INXT_SAM_AAAAAAAAA4, - "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment2, :uuid)}" %>, - "fastq_2": "" - } sample46_irida_next_example_completed_with_output: sample_id: <%= ActiveRecord::FixtureSet.identify(:sample46, :uuid) %> @@ -340,3 +311,39 @@ sample_canceled_unclean_DELETE: "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment_canceled_unclean_DELETE, :uuid)}" %>, "fastq_2": "" } + +samples_workflow_execution_invalid_metadata: + sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> + workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:workflow_execution_invalid_metadata, :uuid) %> + samplesheet_params: + { + "sample": INXT_SAM_AAAAAAAAAA, + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %> + } + +samples_irida_next_example_completing_a: + sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> + workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:irida_next_example_completing_a, :uuid) %> + samplesheet_params: + { + "sample": "INXT_SAM_AAAAAAAAAA", + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %> + } + +samples_automated_example_prepared: + sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> + workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:automated_example_prepared, :uuid) %> + samplesheet_params: + { + "sample": INXT_SAM_AAAAAAAAAA, + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %> + } + +samples_automated_workflow_execution: + sample_id: <%= ActiveRecord::FixtureSet.identify(:sample1, :uuid) %> + workflow_execution_id: <%= ActiveRecord::FixtureSet.identify(:automated_workflow_execution, :uuid) %> + samplesheet_params: + { + "sample": INXT_SAM_AAAAAAAAAA, + "fastq_1": <%= "gid://irida/Attachment/#{ActiveRecord::FixtureSet.identify(:attachment1, :uuid)}" %> + } diff --git a/test/graphql/submit_workflow_execution_mutation_test.rb b/test/graphql/submit_workflow_execution_mutation_test.rb new file mode 100644 index 0000000000..19865e2648 --- /dev/null +++ b/test/graphql/submit_workflow_execution_mutation_test.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require 'test_helper' + +class SubmitWorkflowExecutionMutationTest < ActiveSupport::TestCase + SUBMIT_WORKFLOW_EXECUTION_MUTATION = <<~GRAPHQL + mutation( + $name: String!, + $project_id: ID!, + $samples_workflow_executions_attributes: [JSON!]! + ) { + submitWorkflowExecution( input:{ + name: $name + projectId: $project_id + updateSamples: false + emailNotification: false + workflowName: "phac-nml/iridanextexample" + workflowVersion: "1.0.3" + workflowParams: { + assembler: "stub", + random_seed: 1, + project_name: "assembly" + } + workflowType: "NFL" + workflowTypeVersion:"DSL2" + workflowEngine:"nextflow" + workflowEngineVersion: "23.10.0" + workflowEngineParameters: [ + { + key:"-r", + value:"1.0.3" + } + ] + workflowUrl: "https://github.com/phac-nml/iridanextexample" + samplesWorkflowExecutionsAttributes: $samples_workflow_executions_attributes + }) { + workflowExecution{ + name + id + } + errors{ + message + path + } + } + } + GRAPHQL + + def setup + @user = users(:john_doe) + @project = projects(:project1) + @sample = samples(:sample1) + @attachment1 = attachments(:attachment1) + @attachment2 = attachments(:attachment2) + end + + test 'submit workflow execution mutation should work' do + samples_workflow_executions_attributes = [ + { + sample_id: @sample.to_global_id.to_s, + samplesheet_params: { + sample: @sample.puid, + fastq_1: @attachment1.to_global_id.to_s, # rubocop:disable Naming/VariableNumber + fastq_2: @attachment2.to_global_id.to_s # rubocop:disable Naming/VariableNumber + } + } + ] + + result = IridaSchema.execute(SUBMIT_WORKFLOW_EXECUTION_MUTATION, + context: { current_user: @user }, + variables: { + name: 'my_new_workflow_submission', + project_id: @project.to_global_id.to_s, + samples_workflow_executions_attributes: + }) + + assert_nil result['errors'], 'should work and have no errors.' + + data = result['data']['submitWorkflowExecution'] + assert_not_empty data, 'submit workflow execution type should work' + workflow_execution = data['workflowExecution'] + assert_equal 'my_new_workflow_submission', workflow_execution['name'] + end + + test 'submit workflow execution mutation with non gid project' do + samples_workflow_executions_attributes = [ + { + sample_id: @sample.to_global_id.to_s, + samplesheet_params: { + sample: @sample.puid, + fastq_1: @attachment1.to_global_id.to_s, # rubocop:disable Naming/VariableNumber + fastq_2: @attachment2.to_global_id.to_s # rubocop:disable Naming/VariableNumber + } + } + ] + + result = IridaSchema.execute(SUBMIT_WORKFLOW_EXECUTION_MUTATION, + context: { current_user: @user }, + variables: { + name: 'my_new_workflow_submission', + project_id: 'not a gid', + samples_workflow_executions_attributes: + }) + + assert_not_empty result['errors'], 'should have errors.' + assert_equal 'not a gid is not a valid IRIDA Next ID.', result['errors'][0]['message'] + end + + test 'submit workflow execution mutation with non project gid' do + samples_workflow_executions_attributes = [ + { + sample_id: @sample.to_global_id.to_s, + samplesheet_params: { + sample: @sample.puid, + fastq_1: @attachment1.to_global_id.to_s, # rubocop:disable Naming/VariableNumber + fastq_2: @attachment2.to_global_id.to_s # rubocop:disable Naming/VariableNumber + } + } + ] + + result = IridaSchema.execute(SUBMIT_WORKFLOW_EXECUTION_MUTATION, + context: { current_user: @user }, + variables: { + name: 'my_new_workflow_submission', + project_id: @sample.to_global_id.to_s, + samples_workflow_executions_attributes: + }) + + assert_not_empty result['errors'], 'should have errors.' + assert_equal "#{@sample.to_global_id} is not a valid ID for Project", result['errors'][0]['message'] + end + + test 'submit workflow execution mutation should fail with invalid sample gid' do + samples_workflow_executions_attributes = [ + { + sample_id: 'this is not a gid', + samplesheet_params: { + sample: @sample.puid, + fastq_1: @attachment1.to_global_id.to_s, # rubocop:disable Naming/VariableNumber + fastq_2: @attachment2.to_global_id.to_s # rubocop:disable Naming/VariableNumber + } + } + ] + + result = IridaSchema.execute(SUBMIT_WORKFLOW_EXECUTION_MUTATION, + context: { current_user: @user }, + variables: { + name: 'my_new_workflow_submission', + project_id: @project.to_global_id.to_s, + samples_workflow_executions_attributes: + }) + + assert_not_empty result['errors'], 'should have errors.' + assert_equal 'this is not a gid is not a valid IRIDA Next ID.', result['errors'][0]['message'] + end + + test 'submit workflow execution mutation should fail with non sample gid' do + samples_workflow_executions_attributes = [ + { + sample_id: @project.to_global_id.to_s, + samplesheet_params: { + sample: @sample.puid, + fastq_1: @attachment1.to_global_id.to_s, # rubocop:disable Naming/VariableNumber + fastq_2: @attachment2.to_global_id.to_s # rubocop:disable Naming/VariableNumber + } + } + ] + + result = IridaSchema.execute(SUBMIT_WORKFLOW_EXECUTION_MUTATION, + context: { current_user: @user }, + variables: { + name: 'my_new_workflow_submission', + project_id: @project.to_global_id.to_s, + samples_workflow_executions_attributes: + }) + + assert_not_empty result['errors'], 'should have errors.' + assert_equal "#{@project.to_global_id} is not a valid ID for Sample", result['errors'][0]['message'] + end +end diff --git a/test/models/samples_workflow_executions_test.rb b/test/models/samples_workflow_executions_test.rb index 5316cb5991..99b83af4e7 100644 --- a/test/models/samples_workflow_executions_test.rb +++ b/test/models/samples_workflow_executions_test.rb @@ -7,11 +7,17 @@ def setup @samples_workflow_executions_valid = samples_workflow_executions( :samples_workflow_executions_valid ) - @samples_workflow_executions_invalid_no_sample = samples_workflow_executions( - :samples_workflow_executions_invalid_no_sample + @samples_workflow_executions_invalid_no_sample_puid = samples_workflow_executions( + :samples_workflow_executions_invalid_no_sample_puid ) - @samples_workflow_executions_invalid_no_workflow_execution = samples_workflow_executions( - :samples_workflow_executions_invalid_no_workflow_execution + @samples_workflow_executions_invalid_mismatch_sample_puid = samples_workflow_executions( + :samples_workflow_executions_invalid_mismatch_sample_puid + ) + @samples_workflow_executions_invalid_file_id = samples_workflow_executions( + :samples_workflow_executions_invalid_file_id + ) + @samples_workflow_executions_mismatch_file_id = samples_workflow_executions( + :samples_workflow_executions_mismatch_file_id ) end @@ -19,18 +25,37 @@ def setup assert @samples_workflow_executions_valid.valid? end - test 'invalid no sample' do - assert_not @samples_workflow_executions_invalid_no_sample.valid? - assert_not_nil @samples_workflow_executions_invalid_no_sample.errors - assert_equal ['Sample must exist'], @samples_workflow_executions_invalid_no_sample.errors.full_messages + test 'invalid mismatch puid' do + assert_not @samples_workflow_executions_invalid_mismatch_sample_puid.valid? + assert_not_nil @samples_workflow_executions_invalid_mismatch_sample_puid.errors + expected_error = 'Sample Provided Sample PUID INXT_SAM_AAAAAAAAAB does not match SampleWorkflowExecution Sample PUID INXT_SAM_AAAAAAAAAA' # rubocop:disable Layout/LineLength + assert_equal expected_error, @samples_workflow_executions_invalid_mismatch_sample_puid.errors.full_messages[0] + end + + test 'invalid no sample puid' do + assert_not @samples_workflow_executions_invalid_no_sample_puid.valid? + assert_not_nil @samples_workflow_executions_invalid_no_sample_puid.errors + expected_error = 'Sample No Sample PUID provided' + assert_equal expected_error, @samples_workflow_executions_invalid_no_sample_puid.errors.full_messages[0] + end + + test 'invalid file id' do + assert_not @samples_workflow_executions_invalid_file_id.valid? + assert_not_nil @samples_workflow_executions_invalid_file_id.errors + expected_error = 'Attachment 12345 is not a valid IRIDA Next ID.' + assert_equal( + expected_error, + @samples_workflow_executions_invalid_file_id.errors.full_messages[0] + ) end - test 'invalid no workflow execution' do - assert_not @samples_workflow_executions_invalid_no_workflow_execution.valid? - assert_not_nil @samples_workflow_executions_invalid_no_workflow_execution.errors + test 'mismatch file id' do + assert_not @samples_workflow_executions_mismatch_file_id.valid? + assert_not_nil @samples_workflow_executions_mismatch_file_id.errors + expected_error = 'Attachment Attachment does not belong to Sample INXT_SAM_AAAAAAAAAA.' assert_equal( - ['Workflow execution must exist'], - @samples_workflow_executions_invalid_no_workflow_execution.errors.full_messages + expected_error, + @samples_workflow_executions_mismatch_file_id.errors.full_messages[0] ) end end diff --git a/test/services/workflow_executions/create_service_test.rb b/test/services/workflow_executions/create_service_test.rb index a6c9eefa2e..935a2c68c1 100644 --- a/test/services/workflow_executions/create_service_test.rb +++ b/test/services/workflow_executions/create_service_test.rb @@ -9,6 +9,15 @@ class CreateServiceTest < ActiveStorageTestCase def setup @user = users(:john_doe) @project = projects(:project1) + @sample = samples(:sample1) + @samples_workflow_executions_attributes = { + '0': { + sample_id: @sample.id, + samplesheet_params: { + sample: @sample.puid + } + } + } end test 'test create new workflow execution' do @@ -27,7 +36,8 @@ def setup workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', submitter_id: @user.id, - namespace_id: @project.namespace.id + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } workflow_params2 = { @@ -45,7 +55,8 @@ def setup workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexamplenew2', submitter_id: @user.id, - namespace_id: @project.namespace.id + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } stub_request(:post, 'http://www.example.com/ga4gh/wes/v1/runs') @@ -137,7 +148,8 @@ def setup workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', submitter_id: @user.id, - namespace_id: @project.namespace.id + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } @workflow_execution = WorkflowExecutions::CreateService.new(@user, workflow_params).execute @@ -164,7 +176,8 @@ def setup workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', submitter_id: @user.id, - namespace_id: @project.namespace.id + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } @workflow_execution = WorkflowExecutions::CreateService.new(@user, workflow_params).execute @@ -190,7 +203,8 @@ def setup workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', submitter_id: @user.id, - namespace_id: @project.namespace.id + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } stub_request(:post, 'http://www.example.com/ga4gh/wes/v1/runs').to_return(body: '{ "run_id": "create_run_4" }', @@ -231,7 +245,8 @@ def setup workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', submitter_id: @user.id, - namespace_id: @project.namespace.id + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } stub_request(:post, 'http://www.example.com/ga4gh/wes/v1/runs').to_return(body: '{ "run_id": "create_run_5" }', @@ -274,7 +289,8 @@ def setup workflow_engine_parameters: { engine: 'nextflow', execute_loc: 'azure' }, workflow_url: 'https://github.com/phac-nml/iridanextexample', submitter_id: @user.id, - namespace_id: @project.namespace.id + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } @workflow_execution = WorkflowExecutions::CreateService.new(@user, workflow_params).execute @@ -305,7 +321,8 @@ def setup workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', submitter_id: @user.id, namespace_id: @project.namespace.id, - name: test_name + name: test_name, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } @workflow_execution = WorkflowExecutions::CreateService.new(@user, workflow_params).execute @@ -340,7 +357,8 @@ def setup workflow_engine_version: '23.10.0', workflow_engine_parameters: { '-r': 'dev' }, workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', - submitter_id: user.id + submitter_id: user.id, + samples_workflow_executions_attributes: @samples_workflow_executions_attributes } exception = assert_raises(ActionPolicy::Unauthorized) do @@ -353,5 +371,78 @@ def setup assert_equal I18n.t(:'action_policy.policy.namespaces/project_namespace.submit_workflow?', name: @project.name), exception.result.message end + + test 'create new workflow execution with non matching sample puid in sample sheet' do + samples_workflow_executions_attributes = { + '0': { + sample_id: samples(:sample1).id, + samplesheet_params: { + sample: samples(:sample2).puid + } + } + } + + workflow_params = { + metadata: + { workflow_name: 'phac-nml/iridanextexample', workflow_version: '1.0.2' }, + workflow_params: + { + input: '/blah/samplesheet.csv', + outdir: '/blah/output' + }, + workflow_type: 'NFL', + workflow_type_version: 'DSL2', + workflow_engine: 'nextflow', + workflow_engine_version: '23.10.0', + workflow_engine_parameters: { '-r': 'dev' }, + workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', + submitter_id: @user.id, + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: samples_workflow_executions_attributes + } + + @workflow_execution = WorkflowExecutions::CreateService.new(@user, workflow_params).execute + + assert @workflow_execution.errors.full_messages + .include?('Sample Provided Sample PUID INXT_SAM_AAAAAAAAAB does not match SampleWorkflowExecution Sample PUID INXT_SAM_AAAAAAAAAA') # rubocop:disable Layout/LineLength + assert_enqueued_jobs(0, except: Turbo::Streams::BroadcastStreamJob) + end + + test 'create new workflow execution with non matching attachments to sample' do + samples_workflow_executions_attributes = { + '0': { + sample_id: samples(:sample2).id, + samplesheet_params: { + sample: samples(:sample2).puid, + fastq_1: attachments(:attachment1).to_global_id # belongs to :sample1 # rubocop:disable Naming/VariableNumber + } + } + } + + workflow_params = { + metadata: + { workflow_name: 'phac-nml/iridanextexample', workflow_version: '1.0.2' }, + workflow_params: + { + input: '/blah/samplesheet.csv', + outdir: '/blah/output' + }, + workflow_type: 'NFL', + workflow_type_version: 'DSL2', + workflow_engine: 'nextflow', + workflow_engine_version: '23.10.0', + workflow_engine_parameters: { '-r': 'dev' }, + workflow_url: 'https://github.com/phac-nml/iridanextexamplenew', + submitter_id: @user.id, + namespace_id: @project.namespace.id, + samples_workflow_executions_attributes: samples_workflow_executions_attributes + } + + @workflow_execution = WorkflowExecutions::CreateService.new(@user, workflow_params).execute + + assert @workflow_execution.errors.full_messages + .include?('Attachment Attachment does not belong to Sample INXT_SAM_AAAAAAAAAB.') + assert_enqueued_jobs(0, except: Turbo::Streams::BroadcastStreamJob) + end end end