diff --git a/Gemfile b/Gemfile index eb6283f9..20ca9416 100644 --- a/Gemfile +++ b/Gemfile @@ -79,7 +79,7 @@ gem 'good_job', '~> 4.0' gem 'rotp' gem 'grpc', '~> 1.67' -gem 'tucana', '0.0.58' +gem 'tucana', '0.0.62' gem 'code0-identities', '~> 0.0.3' @@ -91,3 +91,5 @@ gem 'code0-zero_track', '0.0.6' gem 'image_processing', '>= 1.2' gem 'json-schema', '~> 6.0' + +gem 'triangulum', '0.5.2' diff --git a/Gemfile.lock b/Gemfile.lock index 67cb0581..e1e89f30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -161,7 +161,7 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.12.2) + json (2.19.2) json-schema (6.1.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) @@ -203,6 +203,7 @@ GEM nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) + open3 (0.2.1) parallel (1.27.0) parser (3.3.10.0) ast (~> 2.4.1) @@ -376,8 +377,13 @@ GEM test-prof (1.5.0) thor (1.4.0) timeout (0.6.0) + triangulum (0.5.2) + base64 (~> 0.3) + json (~> 2.19) + open3 (~> 0.2) + tucana (~> 0.0, >= 0.0.62) tsort (0.2.0) - tucana (0.0.58) + tucana (0.0.62) grpc (~> 1.64) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -431,7 +437,8 @@ DEPENDENCIES simplecov (~> 0.22.0) simplecov-cobertura (~> 3.0) test-prof (~> 1.0) - tucana (= 0.0.58) + triangulum (= 0.5.2) + tucana (= 0.0.62) tzinfo-data RUBY VERSION diff --git a/app/finders/data_types_finder.rb b/app/finders/data_types_finder.rb index 608cdc7d..740840ed 100644 --- a/app/finders/data_types_finder.rb +++ b/app/finders/data_types_finder.rb @@ -29,7 +29,11 @@ def by_data_type(data_types) def by_runtime_function_definition(data_types) return data_types unless params[:runtime_function_definition] - data_types.where(id: params[:runtime_function_definition].referenced_data_types.pluck(:id)) + referenced_data_types_ids = RuntimeFunctionDefinitionDataTypeLink + .where(runtime_function_definition: params[:runtime_function_definition]) + .select(:referenced_data_type_id) + + data_types.where(id: referenced_data_types_ids) end def by_flow_type_setting(data_types) diff --git a/app/graphql/types/flow_type.rb b/app/graphql/types/flow_type.rb index 265ff5ae..05281d66 100644 --- a/app/graphql/types/flow_type.rb +++ b/app/graphql/types/flow_type.rb @@ -8,6 +8,10 @@ class FlowType < Types::BaseObject field :name, String, null: false, description: 'Name of the flow' + field :validation_status, Types::FlowValidationStatusEnum, + null: false, + description: 'The validation status of the flow' + field :input_type, String, null: true, description: 'The input data type of the flow' diff --git a/app/graphql/types/flow_validation_status_enum.rb b/app/graphql/types/flow_validation_status_enum.rb new file mode 100644 index 00000000..7b3f0b59 --- /dev/null +++ b/app/graphql/types/flow_validation_status_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class FlowValidationStatusEnum < Types::BaseEnum + description 'The validation status of a flow.' + + value :UNVALIDATED, 'The flow has not been validated yet.', value: 'unvalidated' + value :VALID, 'The flow has been validated and is valid.', value: 'valid' + value :INVALID, 'The flow has been validated and is invalid.', value: 'invalid' + end +end diff --git a/app/grpc/flow_handler.rb b/app/grpc/flow_handler.rb index bf181c8a..e570549d 100644 --- a/app/grpc/flow_handler.rb +++ b/app/grpc/flow_handler.rb @@ -10,7 +10,7 @@ class FlowHandler < Tucana::Sagittarius::FlowService::Service def self.update_runtime(runtime) flows = [] runtime.project_assignments.compatible.each do |assignment| - assignment.namespace_project.flows.each do |flow| + assignment.namespace_project.flows.validation_status_valid.each do |flow| flows << flow.to_grpc end end diff --git a/app/jobs/flow_validation_job.rb b/app/jobs/flow_validation_job.rb new file mode 100644 index 00000000..bf865375 --- /dev/null +++ b/app/jobs/flow_validation_job.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class FlowValidationJob < ApplicationJob + def perform(flow_id) + flow = Flow.find_by(id: flow_id) + return if flow.nil? + + Namespaces::Projects::Flows::ValidationService.new(flow).execute + end +end diff --git a/app/models/data_type.rb b/app/models/data_type.rb index 68c33d9b..91cf0629 100644 --- a/app/models/data_type.rb +++ b/app/models/data_type.rb @@ -30,4 +30,18 @@ def validate_version def parsed_version Gem::Version.new(version) end + + def to_grpc + Tucana::Shared::DefinitionDataType.new( + identifier: identifier, + name: names.map(&:to_grpc), + display_message: display_messages.map(&:to_grpc), + alias: aliases.map(&:to_grpc), + rules: rules.map(&:to_grpc), + generic_keys: generic_keys, + type: type, + linked_data_type_identifiers: referenced_data_types.map(&:identifier), + version: version + ) + end end diff --git a/app/models/data_type_rule.rb b/app/models/data_type_rule.rb index 9b7acb20..88571e84 100644 --- a/app/models/data_type_rule.rb +++ b/app/models/data_type_rule.rb @@ -26,4 +26,8 @@ class DataTypeRule < ApplicationRecord filename: 'data_types/RegexRuleConfig', hash_conversion: true, } + + def to_grpc + Tucana::Shared::DefinitionDataTypeRule.create(variant.to_sym, config) + end end diff --git a/app/models/flow.rb b/app/models/flow.rb index f9c1c299..c6026139 100644 --- a/app/models/flow.rb +++ b/app/models/flow.rb @@ -1,16 +1,30 @@ # frozen_string_literal: true class Flow < ApplicationRecord + VALIDATION_STATUS = { + unvalidated: 0, + valid: 1, + invalid: 2, + }.with_indifferent_access + belongs_to :project, class_name: 'NamespaceProject' belongs_to :flow_type belongs_to :starting_node, class_name: 'NodeFunction', optional: true + enum :validation_status, VALIDATION_STATUS, prefix: :validation_status + has_many :flow_settings, class_name: 'FlowSetting', inverse_of: :flow has_many :node_functions, class_name: 'NodeFunction', inverse_of: :flow has_many :flow_data_type_links, inverse_of: :flow has_many :referenced_data_types, through: :flow_data_type_links, source: :referenced_data_type + validates :validation_status, + presence: true, + inclusion: { + in: VALIDATION_STATUS.keys.map(&:to_s), + } + validates :name, presence: true, allow_blank: false, uniqueness: { case_sensitive: false, scope: :project_id } diff --git a/app/models/function_definition.rb b/app/models/function_definition.rb index ecd0a9aa..e65fc674 100644 --- a/app/models/function_definition.rb +++ b/app/models/function_definition.rb @@ -14,4 +14,8 @@ class FunctionDefinition < ApplicationRecord has_many :display_messages, -> { by_purpose(:display_message) }, class_name: 'Translation', as: :owner, inverse_of: :owner has_many :aliases, -> { by_purpose(:alias) }, class_name: 'Translation', as: :owner, inverse_of: :owner + + delegate :to_grpc, to: :runtime_function_definition + + scope :by_node_function, ->(node_functions) { where(node_functions: node_functions) } end diff --git a/app/models/runtime_function_definition.rb b/app/models/runtime_function_definition.rb index ecb6b8f9..e2d860eb 100644 --- a/app/models/runtime_function_definition.rb +++ b/app/models/runtime_function_definition.rb @@ -39,4 +39,21 @@ def validate_version def parsed_version Gem::Version.new(version) end + + def to_grpc + Tucana::Shared::RuntimeFunctionDefinition.new( + runtime_name: runtime_name, + runtime_parameter_definitions: parameters.map(&:to_grpc), + signature: signature, + throws_error: throws_error, + name: names.map(&:to_grpc), + description: descriptions.map(&:to_grpc), + documentation: documentations.map(&:to_grpc), + deprecation_message: deprecation_messages.map(&:to_grpc), + display_message: display_messages.map(&:to_grpc), + alias: aliases.map(&:to_grpc), + linked_data_type_identifiers: referenced_data_types.map(&:identifier), + version: version + ) + end end diff --git a/app/models/runtime_parameter_definition.rb b/app/models/runtime_parameter_definition.rb index a6a5724c..c0322ffe 100644 --- a/app/models/runtime_parameter_definition.rb +++ b/app/models/runtime_parameter_definition.rb @@ -11,4 +11,14 @@ class RuntimeParameterDefinition < ApplicationRecord validates :runtime_name, length: { minimum: 3, maximum: 50 }, presence: true, uniqueness: { case_sensitive: false, scope: :runtime_function_definition_id } + + def to_grpc + Tucana::Shared::RuntimeParameterDefinition.new( + runtime_name: runtime_name, + default_value: Tucana::Shared::Value.from_ruby(default_value), + name: names.map(&:to_grpc), + description: descriptions.map(&:to_grpc), + documentation: documentations.map(&:to_grpc) + ) + end end diff --git a/app/models/translation.rb b/app/models/translation.rb index 62200a91..22487ebe 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -7,4 +7,11 @@ class Translation < ApplicationRecord validates :content, presence: true scope :by_purpose, ->(purpose) { where(purpose: purpose) } + + def to_grpc + Tucana::Shared::Translation.new( + code: code, + content: content + ) + end end diff --git a/app/services/namespaces/projects/flows/update_service.rb b/app/services/namespaces/projects/flows/update_service.rb index ce8feb3c..bf053af9 100644 --- a/app/services/namespaces/projects/flows/update_service.rb +++ b/app/services/namespaces/projects/flows/update_service.rb @@ -41,7 +41,7 @@ def update_flow(t) ) end - UpdateRuntimesForProjectJob.perform_later(flow.project.id) + FlowValidationJob.perform_later(flow.id) end private @@ -50,6 +50,7 @@ def update_flow_attributes flow.name = flow_input.name flow.input_type = flow_input.input_type flow.return_type = flow_input.return_type + flow.validation_status = :unvalidated end def update_settings(t) diff --git a/app/services/namespaces/projects/flows/validation_service.rb b/app/services/namespaces/projects/flows/validation_service.rb new file mode 100644 index 00000000..0545a9dc --- /dev/null +++ b/app/services/namespaces/projects/flows/validation_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Namespaces + module Projects + module Flows + class ValidationService + attr_reader :flow + + def initialize(flow) + @flow = flow + end + + def execute + function_definitions = FunctionDefinition + .by_node_function(flow.node_functions) + .preload(:runtime_function_definition) + data_types = DataTypesFinder.new( + { + runtime_function_definition: function_definitions.map(&:runtime_function_definition), + expand_recursively: true, + } + ).execute + + result = Triangulum::Validation.new( + flow.to_grpc, + function_definitions.map(&:to_grpc), + data_types.map(&:to_grpc) + ).validate + + if result.valid? + flow.update!(validation_status: :valid) + else + flow.update!(validation_status: :invalid) + end + + UpdateRuntimesForProjectJob.perform_later(flow.project.id) + end + end + end + end +end diff --git a/db/migrate/20260324182151_add_validation_status_to_flows.rb b/db/migrate/20260324182151_add_validation_status_to_flows.rb new file mode 100644 index 00000000..1ca4bea5 --- /dev/null +++ b/db/migrate/20260324182151_add_validation_status_to_flows.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddValidationStatusToFlows < Code0::ZeroTrack::Database::Migration[1.0] + def change + add_column :flows, :validation_status, :integer, null: false, default: 0 + end +end diff --git a/db/schema_migrations/20260324182151 b/db/schema_migrations/20260324182151 new file mode 100644 index 00000000..2d68c040 --- /dev/null +++ b/db/schema_migrations/20260324182151 @@ -0,0 +1 @@ +2ce443053ded376e78875486212da5a7ecdda429d0dd1434749bceffa4fc18dd \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 76809bdb..47bcfce1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -303,6 +303,7 @@ CREATE TABLE flows ( updated_at timestamp with time zone NOT NULL, input_type text, return_type text, + validation_status integer DEFAULT 0 NOT NULL, CONSTRAINT check_1c805d704f CHECK ((char_length(input_type) <= 2000)), CONSTRAINT check_b2f3f83908 CHECK ((char_length(return_type) <= 2000)) ); diff --git a/docs/graphql/enum/flowvalidationstatus.md b/docs/graphql/enum/flowvalidationstatus.md new file mode 100644 index 00000000..d1793444 --- /dev/null +++ b/docs/graphql/enum/flowvalidationstatus.md @@ -0,0 +1,11 @@ +--- +title: FlowValidationStatus +--- + +The validation status of a flow. + +| Value | Description | +|-------|-------------| +| `INVALID` | The flow has been validated and is invalid. | +| `UNVALIDATED` | The flow has not been validated yet. | +| `VALID` | The flow has been validated and is valid. | diff --git a/docs/graphql/object/flow.md b/docs/graphql/object/flow.md index a581389b..a1740544 100644 --- a/docs/graphql/object/flow.md +++ b/docs/graphql/object/flow.md @@ -21,4 +21,5 @@ Represents a flow | `type` | [`FlowType!`](../object/flowtype.md) | The flow type of the flow | | `updatedAt` | [`Time!`](../scalar/time.md) | Time when this Flow was last updated | | `userAbilities` | [`FlowUserAbilities!`](../object/flowuserabilities.md) | Abilities for the current user on this Flow | +| `validationStatus` | [`FlowValidationStatus!`](../enum/flowvalidationstatus.md) | The validation status of the flow | diff --git a/spec/factories/flows.rb b/spec/factories/flows.rb index 7da85541..8c987464 100644 --- a/spec/factories/flows.rb +++ b/spec/factories/flows.rb @@ -6,6 +6,7 @@ factory :flow do project factory: :namespace_project flow_type + validation_status { :unvalidated } starting_node { nil } flow_settings { [] } input_type { 'string' } diff --git a/spec/jobs/flow_validation_job_spec.rb b/spec/jobs/flow_validation_job_spec.rb new file mode 100644 index 00000000..b763c2cb --- /dev/null +++ b/spec/jobs/flow_validation_job_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FlowValidationJob do + include ActiveJob::TestHelper + + let(:flow) { create(:flow) } + + it 'calls the validation service' do + service = instance_double(Namespaces::Projects::Flows::ValidationService) + allow(Namespaces::Projects::Flows::ValidationService).to receive(:new).with(flow).and_return(service) + allow(service).to receive(:execute) + + perform_enqueued_jobs do + described_class.perform_later(flow.id) + end + + expect(service).to have_received(:execute) + end + + it 'does not raise when flow does not exist' do + expect do + perform_enqueued_jobs do + described_class.perform_later(-1) + end + end.not_to raise_error + end +end diff --git a/spec/models/data_type_rule_spec.rb b/spec/models/data_type_rule_spec.rb index 068699cd..cd0cd211 100644 --- a/spec/models/data_type_rule_spec.rb +++ b/spec/models/data_type_rule_spec.rb @@ -56,4 +56,10 @@ end end end + + describe '#to_grpc' do + it 'returns a grpc rule' do + expect(rule.to_grpc).to be_a(Tucana::Shared::DefinitionDataTypeRule) + end + end end diff --git a/spec/models/data_type_spec.rb b/spec/models/data_type_spec.rb index b9812290..e859f3ac 100644 --- a/spec/models/data_type_spec.rb +++ b/spec/models/data_type_spec.rb @@ -42,4 +42,29 @@ end end end + + describe '#to_grpc' do + let!(:name) { create(:translation, owner: data_type, purpose: :name, code: 'en', content: 'Name') } + let!(:display) { create(:translation, owner: data_type, purpose: :display_message, code: 'en', content: 'Disp') } + let!(:alias_t) { create(:translation, owner: data_type, purpose: :alias, code: 'en', content: 'Ali') } + let!(:rule) { create(:data_type_rule, data_type: data_type) } + let!(:ref_data_type) { create(:data_type, runtime: data_type.runtime) } + + before { create(:data_type_data_type_link, data_type: data_type, referenced_data_type: ref_data_type) } + + it 'matches the model' do + grpc_object = data_type.to_grpc + + expect(grpc_object.to_h).to eq( + identifier: data_type.identifier, + name: [name.to_grpc.to_h], + display_message: [display.to_grpc.to_h], + alias: [alias_t.to_grpc.to_h], + rules: [rule.to_grpc.to_h], + type: data_type.type, + linked_data_type_identifiers: [ref_data_type.identifier], + version: data_type.version + ) + end + end end diff --git a/spec/models/flow_spec.rb b/spec/models/flow_spec.rb index 278788e9..b8017ae3 100644 --- a/spec/models/flow_spec.rb +++ b/spec/models/flow_spec.rb @@ -19,6 +19,8 @@ end describe 'validations' do + it { is_expected.to allow_values(*described_class::VALIDATION_STATUS.keys).for(:validation_status) } + it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to(:project_id) } @@ -26,6 +28,18 @@ it { is_expected.to validate_length_of(:return_type).is_at_most(2000) } end + describe 'scopes' do + describe 'validation status' do + let!(:unvalidated_flow) { create(:flow, validation_status: :unvalidated) } + let!(:valid_flow) { create(:flow, validation_status: :valid) } + let!(:invalid_flow) { create(:flow, validation_status: :invalid) } + + it { expect(described_class.validation_status_unvalidated).to contain_exactly(unvalidated_flow) } + it { expect(described_class.validation_status_valid).to contain_exactly(valid_flow) } + it { expect(described_class.validation_status_invalid).to contain_exactly(invalid_flow) } + end + end + describe '#to_grpc' do let(:flow) do create( diff --git a/spec/models/runtime_function_definition_spec.rb b/spec/models/runtime_function_definition_spec.rb index 3b2bed95..fc8c5d31 100644 --- a/spec/models/runtime_function_definition_spec.rb +++ b/spec/models/runtime_function_definition_spec.rb @@ -60,4 +60,41 @@ it { is_expected.to have_many(:documentations).class_name('Translation').inverse_of(:owner) } it { is_expected.to have_many(:deprecation_messages).class_name('Translation').inverse_of(:owner) } end + + describe '#to_grpc' do + let!(:param) { create(:runtime_parameter_definition, runtime_function_definition: function) } + let!(:name) { create(:translation, owner: function, purpose: :name, code: 'en', content: 'Name') } + let!(:description) { create(:translation, owner: function, purpose: :description, code: 'en', content: 'Desc') } + let!(:documentation) { create(:translation, owner: function, purpose: :documentation, code: 'en', content: 'Doc') } + let!(:deprecation) do + create(:translation, owner: function, purpose: :deprecation_message, code: 'en', content: 'Dep') + end + let!(:display) { create(:translation, owner: function, purpose: :display_message, code: 'en', content: 'Disp') } + let!(:alias_t) { create(:translation, owner: function, purpose: :alias, code: 'en', content: 'Ali') } + let!(:data_type) { create(:data_type, runtime: function.runtime) } + + before do + create(:runtime_function_definition_data_type_link, + runtime_function_definition: function, referenced_data_type: data_type) + end + + it 'matches the model' do + grpc_object = function.to_grpc + + expect(grpc_object.to_h).to eq( + runtime_name: function.runtime_name, + runtime_parameter_definitions: [param.to_grpc.to_h], + signature: function.signature, + throws_error: function.throws_error, + name: [name.to_grpc.to_h], + description: [description.to_grpc.to_h], + documentation: [documentation.to_grpc.to_h], + deprecation_message: [deprecation.to_grpc.to_h], + display_message: [display.to_grpc.to_h], + alias: [alias_t.to_grpc.to_h], + linked_data_type_identifiers: [data_type.identifier], + version: function.version + ) + end + end end diff --git a/spec/models/runtime_parameter_definition_spec.rb b/spec/models/runtime_parameter_definition_spec.rb index db263096..1bee538a 100644 --- a/spec/models/runtime_parameter_definition_spec.rb +++ b/spec/models/runtime_parameter_definition_spec.rb @@ -22,4 +22,24 @@ it { is_expected.to have_many(:descriptions).class_name('Translation').inverse_of(:owner) } it { is_expected.to have_many(:documentations).class_name('Translation').inverse_of(:owner) } end + + describe '#to_grpc' do + subject(:parameter) { create(:runtime_parameter_definition) } + + let!(:name) { create(:translation, owner: parameter, purpose: :name, code: 'en', content: 'Name') } + let!(:description) { create(:translation, owner: parameter, purpose: :description, code: 'en', content: 'Desc') } + let!(:documentation) { create(:translation, owner: parameter, purpose: :documentation, code: 'en', content: 'Doc') } + + it 'matches the model' do + grpc_object = parameter.to_grpc + + expect(grpc_object.to_h).to eq( + runtime_name: parameter.runtime_name, + default_value: Tucana::Shared::Value.from_ruby(parameter.default_value).to_h, + name: [name.to_grpc.to_h], + description: [description.to_grpc.to_h], + documentation: [documentation.to_grpc.to_h] + ) + end + end end diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb index 8c5fe0bc..b50da044 100644 --- a/spec/models/translation_spec.rb +++ b/spec/models/translation_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Translation do - subject { create(:translation) } + subject(:translation) { create(:translation) } describe 'associations' do it { is_expected.to belong_to(:owner).required } @@ -13,4 +13,15 @@ it { is_expected.to validate_presence_of(:code) } it { is_expected.to validate_presence_of(:content) } end + + describe '#to_grpc' do + it 'matches the model' do + grpc_object = translation.to_grpc + + expect(grpc_object.to_h).to eq( + code: translation.code, + content: translation.content + ) + end + end end diff --git a/spec/services/namespaces/projects/flows/create_service_spec.rb b/spec/services/namespaces/projects/flows/create_service_spec.rb index d28b2bb5..b4d475b3 100644 --- a/spec/services/namespaces/projects/flows/create_service_spec.rb +++ b/spec/services/namespaces/projects/flows/create_service_spec.rb @@ -113,12 +113,12 @@ ) end - it 'queues job to update runtimes' do - allow(UpdateRuntimesForProjectJob).to receive(:perform_later) + it 'queues job to validate flow' do + allow(FlowValidationJob).to receive(:perform_later) - service_response + flow_id = service_response.payload.id - expect(UpdateRuntimesForProjectJob).to have_received(:perform_later).with(namespace_project.id) + expect(FlowValidationJob).to have_received(:perform_later).with(flow_id) end end end diff --git a/spec/services/namespaces/projects/flows/update_service_spec.rb b/spec/services/namespaces/projects/flows/update_service_spec.rb index 60b0769b..0aa5622d 100644 --- a/spec/services/namespaces/projects/flows/update_service_spec.rb +++ b/spec/services/namespaces/projects/flows/update_service_spec.rb @@ -94,12 +94,12 @@ ) end - it 'queues job to update runtimes' do - allow(UpdateRuntimesForProjectJob).to receive(:perform_later) + it 'queues job to validate flow' do + allow(FlowValidationJob).to receive(:perform_later) service_response - expect(UpdateRuntimesForProjectJob).to have_received(:perform_later).with(namespace_project.id) + expect(FlowValidationJob).to have_received(:perform_later).with(flow.id) end end end diff --git a/spec/services/namespaces/projects/flows/validation_service_spec.rb b/spec/services/namespaces/projects/flows/validation_service_spec.rb new file mode 100644 index 00000000..b3b7213e --- /dev/null +++ b/spec/services/namespaces/projects/flows/validation_service_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Namespaces::Projects::Flows::ValidationService do + subject(:service) { described_class.new(flow) } + + let(:runtime) { create(:runtime) } + let(:namespace_project) { create(:namespace_project) } + let(:flow_type) { create(:flow_type, runtime: runtime) } + let(:runtime_function_definition) { create(:runtime_function_definition, runtime: runtime) } + let(:function_definition) { create(:function_definition, runtime_function_definition: runtime_function_definition) } + let(:node_function) { create(:node_function, flow: flow, function_definition: function_definition) } + let(:flow) do + create(:flow, project: namespace_project, flow_type: flow_type, validation_status: :unvalidated, + starting_node: nil) + end + + before do + flow.update!(starting_node: node_function) + allow(UpdateRuntimesForProjectJob).to receive(:perform_later) + + result = Triangulum::Validation::Result.new(valid?: valid, return_type: nil, diagnostics: []) + allow(Triangulum::Validation).to receive(:new).and_return( + instance_double(Triangulum::Validation, validate: result) + ) + end + + context 'when validation passes' do + let(:valid) { true } + + it 'sets validation status to valid' do + service.execute + + expect(flow.reload.validation_status).to eq('valid') + end + + it 'enqueues UpdateRuntimesForProjectJob' do + service.execute + + expect(UpdateRuntimesForProjectJob).to have_received(:perform_later).with(namespace_project.id) + end + end + + context 'when validation fails' do + let(:valid) { false } + + it 'sets validation status to invalid' do + service.execute + + expect(flow.reload.validation_status).to eq('invalid') + end + + it 'enqueues UpdateRuntimesForProjectJob' do + service.execute + + expect(UpdateRuntimesForProjectJob).to have_received(:perform_later).with(namespace_project.id) + end + end +end