diff --git a/lib/inferno/db/migrations/010_add_validator_sessions.rb b/lib/inferno/db/migrations/010_add_validator_sessions.rb new file mode 100644 index 000000000..78b9b7976 --- /dev/null +++ b/lib/inferno/db/migrations/010_add_validator_sessions.rb @@ -0,0 +1,14 @@ +Sequel.migration do + change do + create_table :validator_sessions do + column :id, String, primary_key: true, null: false, size: 36 + column :validator_session_id, String, null: false, size: 255 + column :test_suite_id, String, null: false, size: 255 + column :validator_name, String, null: false, size: 255 + column :suite_options, String, text: true + column :last_accessed, DateTime, null: false + index [:validator_session_id], unique: true + index [:test_suite_id, :validator_name, :suite_options], unique: true + end + end +end diff --git a/lib/inferno/db/schema.rb b/lib/inferno/db/schema.rb index 324dd96cb..ab222d044 100644 --- a/lib/inferno/db/schema.rb +++ b/lib/inferno/db/schema.rb @@ -23,6 +23,20 @@ primary_key [:id] end + create_table(:validator_sessions, :ignore_index_errors=>true) do + String :id, :size=>36, :null=>false + String :validator_session_id, :size=>255, :null=>false + String :test_suite_id, :size=>255, :null=>false + String :validator_name, :size=>255, :null=>false + String :suite_options, :text=>true + DateTime :last_accessed, :null=>false + + primary_key [:id] + + index [:test_suite_id, :validator_name, :suite_options], :unique=>true + index [:validator_session_id], :unique=>true + end + create_table(:session_data, :ignore_index_errors=>true) do String :id, :size=>255, :null=>false foreign_key :test_session_id, :test_sessions, :type=>String, :size=>36, :null=>false, :key=>[:id] diff --git a/lib/inferno/dsl/fhir_resource_validation.rb b/lib/inferno/dsl/fhir_resource_validation.rb index e7bd2598f..642fef6ee 100644 --- a/lib/inferno/dsl/fhir_resource_validation.rb +++ b/lib/inferno/dsl/fhir_resource_validation.rb @@ -27,10 +27,12 @@ def self.included(klass) class Validator attr_reader :requirements - attr_accessor :session_id + attr_accessor :session_id, :name, :test_suite_id # @private - def initialize(requirements = nil, &) + def initialize(name = nil, test_suite_id = nil, requirements = nil, &) + @name = name + @test_suite_id = test_suite_id instance_eval(&) @requirements = requirements end @@ -40,6 +42,10 @@ def default_validator_url ENV.fetch('FHIR_RESOURCE_VALIDATOR_URL') end + def validator_session_repo + @validator_session_repo ||= Inferno::Repositories::ValidatorSessions.new + end + # Set the url of the validator service # # @param validator_url [String] @@ -231,6 +237,10 @@ def issue_message(issue, resource) # @private def wrap_resource_for_hl7_wrapper(resource, profile_url) + validator_session_id = + validator_session_repo.find_validator_session_id(test_suite_id, + name.to_s, requirements) + @session_id = validator_session_id if validator_session_id wrapped_resource = { cliContext: { **cli_context.definition, @@ -269,8 +279,11 @@ def call_validator(resource, profile_url) # @private def operation_outcome_from_hl7_wrapped_response(response) res = JSON.parse(response) - - @session_id = res['sessionId'] + if res['sessionId'] != @session_id + validator_session_repo.save(test_suite_id:, validator_session_id: res['sessionId'], + validator_name: name.to_s, suite_options: requirements) + @session_id = res['sessionId'] + end # assume for now that one resource -> one request issues = res['outcomes'][0]['issues']&.map do |i| @@ -287,7 +300,7 @@ def operation_outcome_from_validator_response(response, runnable) else runnable.add_message('error', "Validator Response: HTTP #{response.status}\n#{response.body}") raise Inferno::Exceptions::ErrorInValidatorException, - 'Validator response was an unexpected format. '\ + 'Validator response was an unexpected format. ' \ 'Review Messages tab or validator service logs for more information.' end end @@ -353,7 +366,7 @@ def fhir_validators def fhir_resource_validator(name = :default, required_suite_options: nil, &block) current_validators = fhir_validators[name] || [] - new_validator = Inferno::DSL::FHIRResourceValidation::Validator.new(required_suite_options, &block) + new_validator = Inferno::DSL::FHIRResourceValidation::Validator.new(name, id, required_suite_options, &block) current_validators.reject! { |validator| validator.requirements == required_suite_options } current_validators << new_validator diff --git a/lib/inferno/entities.rb b/lib/inferno/entities.rb index 8138be9e4..3158719b0 100644 --- a/lib/inferno/entities.rb +++ b/lib/inferno/entities.rb @@ -11,6 +11,7 @@ require_relative 'entities/test_run' require_relative 'entities/test_session' require_relative 'entities/test_suite' +require_relative 'entities/validator_session' module Inferno # Entities are domain objects whose identity is based on an `id`. Entities diff --git a/lib/inferno/entities/validator_session.rb b/lib/inferno/entities/validator_session.rb new file mode 100644 index 000000000..61eb5f36d --- /dev/null +++ b/lib/inferno/entities/validator_session.rb @@ -0,0 +1,22 @@ +module Inferno + module Entities + class ValidatorSession < Entity + ATTRIBUTES = [ + :id, + :created_at, + :updated_at, + :validator_session_id, + :test_suite_id, + :validator_name, + :suite_options, + :validator_index + ].freeze + + include Inferno::Entities::Attributes + + def initialize(params) + super(params, ATTRIBUTES) + end + end + end +end diff --git a/lib/inferno/jobs/invoke_validator_session.rb b/lib/inferno/jobs/invoke_validator_session.rb index 314017e21..72c095fb7 100644 --- a/lib/inferno/jobs/invoke_validator_session.rb +++ b/lib/inferno/jobs/invoke_validator_session.rb @@ -6,13 +6,13 @@ class InvokeValidatorSession def perform(suite_id, validator_name, validator_index) suite = Inferno::Repositories::TestSuites.new.find suite_id validator = suite.fhir_validators[validator_name.to_sym][validator_index] - response_body = validator.validate(FHIR::Patient.new, 'http://hl7.org/fhir/StructureDefinition/Patient') - if response_body.start_with? '{' res = JSON.parse(response_body) session_id = res['sessionId'] - # TODO: (FI-2311) store this session ID so it can be referenced as needed + session_repo = Inferno::Repositories::ValidatorSessions.new + session_repo.save(test_suite_id: suite_id, validator_session_id: session_id, + validator_name:, suite_options: validator.requirements) validator.session_id = session_id else Inferno::Application['logger'].error("InvokeValidatorSession - error from validator: #{response_body}") diff --git a/lib/inferno/repositories.rb b/lib/inferno/repositories.rb index cb2e2de29..aecea7367 100644 --- a/lib/inferno/repositories.rb +++ b/lib/inferno/repositories.rb @@ -15,6 +15,7 @@ require_relative 'repositories/session_data' require_relative 'repositories/test_runs' require_relative 'repositories/test_sessions' + require_relative 'repositories/validator_sessions' end module Inferno diff --git a/lib/inferno/repositories/validator_sessions.rb b/lib/inferno/repositories/validator_sessions.rb new file mode 100644 index 000000000..fa6d5c0e2 --- /dev/null +++ b/lib/inferno/repositories/validator_sessions.rb @@ -0,0 +1,58 @@ +require 'base62-rb' +require_relative 'repository' + +module Inferno + module Repositories + class ValidatorSessions < Repository + def save(params) + validator_session_id = params[:validator_session_id] + validator_name = params[:validator_name] + test_suite_id = params[:test_suite_id] + raw_suite_options = params[:suite_options] + time = Time.now + + suite_options = + if raw_suite_options.blank? + '[]' + else + raw_suite_options.sort.to_s + end + + db.insert_conflict( + target: [:test_suite_id, + :suite_options, + :validator_name], + update: { validator_session_id:, + test_suite_id:, + suite_options:, + validator_name: } + ).insert( + id: "#{validator_session_id}_#{validator_name}", + validator_session_id:, + test_suite_id:, + validator_name:, + suite_options:, + last_accessed: time + ) + end + + def find_validator_session_id(test_suite_id, validator_name, suite_options) + suite_options = suite_options.nil? ? '[]' : suite_options.sort.to_s + session = self.class::Model + .find(test_suite_id:, validator_name:, suite_options:) + return nil if session.nil? + + time = Time.now + session.update(last_accessed: time) + session[:validator_session_id] + end + + class Model < Sequel::Model(db) + def before_save + time = Time.now + self.last_accessed ||= time + end + end + end + end +end diff --git a/spec/inferno/dsl/fhir_resource_validation_spec.rb b/spec/inferno/dsl/fhir_resource_validation_spec.rb index 98c1b3893..3fe319fd5 100644 --- a/spec/inferno/dsl/fhir_resource_validation_spec.rb +++ b/spec/inferno/dsl/fhir_resource_validation_spec.rb @@ -2,7 +2,7 @@ let(:validation_url) { 'http://example.com' } let(:profile_url) { 'PROFILE' } let(:validator) do - Inferno::DSL::FHIRResourceValidation::Validator.new do + Inferno::DSL::FHIRResourceValidation::Validator.new('test_validator', 'test_suite') do url 'http://example.com' end end @@ -195,13 +195,13 @@ }) end - expect(v1.cli_context.definition.fetch(:txServer, :missing)).to eq(nil) + expect(v1.cli_context.definition.fetch(:txServer, :missing)).to be_nil expect(v1.cli_context.definition.fetch(:displayWarnings, :missing)).to eq(:missing) - expect(v1.cli_context.txServer).to eq(nil) + expect(v1.cli_context.txServer).to be_nil expect(v2.cli_context.definition.fetch(:txServer, :missing)).to eq(:missing) - expect(v2.cli_context.definition[:displayWarnings]).to eq(true) - expect(v2.cli_context.displayWarnings).to eq(true) + expect(v2.cli_context.definition[:displayWarnings]).to be(true) + expect(v2.cli_context.displayWarnings).to be(true) expect(v3.cli_context.igs).to eq(['hl7.fhir.us.core#1.0.1']) expect(v3.cli_context.extensions).to eq([]) diff --git a/spec/inferno/repositories/validator_sessions_spec.rb b/spec/inferno/repositories/validator_sessions_spec.rb new file mode 100644 index 000000000..fdea4493b --- /dev/null +++ b/spec/inferno/repositories/validator_sessions_spec.rb @@ -0,0 +1,165 @@ +RSpec.describe Inferno::Repositories::ValidatorSessions do + let(:repo) { described_class.new } + let(:validator_name) { 'basic_name' } + let(:test_suite_id) { 'basic_suite' } + let(:validator_session_id1) { 'basic_validator1' } + let(:validator_session_id2) { 'basic_validator2' } + let(:validator_session_id3) { 'basic_validator3' } + let(:suite_options1) { { ig_version: '1', us_core_version: '4' } } + let(:suite_options2) { { ig_version: '2' } } + let(:suite_options_alt) { { us_core_version: '4', ig_version: '1' } } + let(:session1_params) do + { + validator_session_id: validator_session_id1, + validator_name:, + test_suite_id:, + suite_options: suite_options1 + } + end + let(:session2_params) do + { + validator_session_id: validator_session_id2, + validator_name:, + test_suite_id:, + suite_options: suite_options2 + } + end + let(:session3_params) do + { + validator_session_id: validator_session_id3, + validator_name:, + test_suite_id:, + suite_options: suite_options1 + } + end + let(:session_params_alt_options) do + { + validator_session_id: validator_session_id2, + validator_name:, + test_suite_id:, + suite_options: suite_options_alt + } + end + let(:session_params_alt_name) do + { + validator_session_id: validator_session_id2, + validator_name: 'alt name', + test_suite_id:, + suite_options: suite_options1 + } + end + let(:session_params_alt_id) do + { + validator_session_id: validator_session_id2, + validator_name:, + test_suite_id: 'alt id', + suite_options: suite_options1 + } + end + + describe '#create' do + before do + repo.save(session1_params) + end + + context 'with valid params' do + it 'persists data' do + record = repo.db.first + expect(repo.db.count).to eq(1) + expect(record[:validator_session_id]).to eq(validator_session_id1) + expect(record[:validator_name]).to eq(validator_name) + expect(record[:test_suite_id]).to eq(test_suite_id) + end + + it 'creates a separate record given a different validator session id' do + repo.save(session2_params) + record = repo.db.all[1] + expect(repo.db.count).to eq(2) + expect(record[:validator_session_id]).to eq(validator_session_id2) + expect(record[:validator_name]).to eq(validator_name) + expect(record[:test_suite_id]).to eq(test_suite_id) + end + + it 'overwrites an existing record given the same validator session id' do + repo.save(session3_params) + record = repo.db.first + expect(repo.db.count).to eq(1) + expect(record[:validator_session_id]).to eq(validator_session_id3) + expect(record[:validator_name]).to eq('basic_name') + expect(record[:test_suite_id]).to eq(test_suite_id) + end + end + end + + describe '#find' do + before do + repo.save(session1_params) + end + + context 'with valid params' do + it 'finds a single record' do + record = repo.db.first + validator_id = repo.find_validator_session_id(session1_params[:test_suite_id], + session1_params[:validator_name], + session1_params[:suite_options]) + expect(record[:validator_session_id]).to eq(validator_id) + end + + it 'updates validator session id when reverse order suite options cause overwrite' do + repo.save(session_params_alt_options) + record = repo.db.first + validator_id = repo.find_validator_session_id(session1_params[:test_suite_id], + session1_params[:validator_name], + session1_params[:suite_options]) + validator_id_alt = repo.find_validator_session_id(session_params_alt_options[:test_suite_id], + session_params_alt_options[:validator_name], + session_params_alt_options[:suite_options]) + expect(validator_session_id2).to eq(validator_id) + expect(validator_session_id2).to eq(validator_id_alt) + expect(validator_session_id2).to eq(record[:validator_session_id]) + end + + it 'saves two records and finds the correct one, discriminating by suite options' do + repo.save(session2_params) + record = repo.db.first + record2 = repo.db.all[1] + validator_id = repo.find_validator_session_id(session1_params[:test_suite_id], + session1_params[:validator_name], + session1_params[:suite_options]) + validator_id2 = repo.find_validator_session_id(session2_params[:test_suite_id], + session2_params[:validator_name], + session2_params[:suite_options]) + expect(record[:validator_session_id]).to eq(validator_id) + expect(record2[:validator_session_id]).to eq(validator_id2) + end + + it 'saves two records and finds the correct one, discriminating by validator name' do + repo.save(session_params_alt_name) + record = repo.db.first + record2 = repo.db.all[1] + validator_id = repo.find_validator_session_id(session1_params[:test_suite_id], + session1_params[:validator_name], + session1_params[:suite_options]) + validator_id2 = repo.find_validator_session_id(session_params_alt_name[:test_suite_id], + session_params_alt_name[:validator_name], + session_params_alt_name[:suite_options]) + expect(record[:validator_session_id]).to eq(validator_id) + expect(record2[:validator_session_id]).to eq(validator_id2) + end + + it 'saves two records and finds the correct one, discriminating by suite id' do + repo.save(session_params_alt_id) + record = repo.db.first + record2 = repo.db.all[1] + validator_id = repo.find_validator_session_id(session1_params[:test_suite_id], + session1_params[:validator_name], + session1_params[:suite_options]) + validator_id2 = repo.find_validator_session_id(session_params_alt_id[:test_suite_id], + session_params_alt_id[:validator_name], + session_params_alt_id[:suite_options]) + expect(record[:validator_session_id]).to eq(validator_id) + expect(record2[:validator_session_id]).to eq(validator_id2) + end + end + end +end