From b2dd4fc532dab0b48fc80a48ddbf6d407ba8236f Mon Sep 17 00:00:00 2001 From: Stephen MacVicar Date: Mon, 4 Dec 2023 11:09:15 -0500 Subject: [PATCH] FI-2330: Request tags (#407) * add migration for tags * add/update entities and repositories to persist tags * WIP * update sql for finding requests via tags * add basic unit tests for tagged requests * add spec to verify that only requests from most recent results are returned * fix sql * add tags to request dsl methods * add example tests to demo suite * update docs * fix linting errors * change add_tag params --- .../dev_demo_ig_stu1/groups/demo_group.rb | 15 +++ .../db/migrations/009_add_request_tags.rb | 18 +++ lib/inferno/db/schema.rb | 19 +++ lib/inferno/dsl/fhir_client.rb | 74 ++++++++---- lib/inferno/dsl/http_client.rb | 20 ++-- lib/inferno/dsl/request_storage.rb | 18 ++- lib/inferno/dsl/resume_test_route.rb | 8 +- lib/inferno/dsl/runnable.rb | 6 +- lib/inferno/entities/request.rb | 30 +++-- lib/inferno/repositories/requests.rb | 110 ++++++++++++++++-- lib/inferno/repositories/tags.rb | 18 +++ spec/factories/request.rb | 2 + spec/inferno/dsl/fhir_client_spec.rb | 7 ++ spec/inferno/dsl/http_client_spec.rb | 11 ++ spec/inferno/repositories/requests_spec.rb | 82 ++++++++++--- 15 files changed, 365 insertions(+), 73 deletions(-) create mode 100644 lib/inferno/db/migrations/009_add_request_tags.rb create mode 100644 lib/inferno/repositories/tags.rb diff --git a/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb b/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb index 330ebfd4b..20c662471 100644 --- a/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb +++ b/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb @@ -330,5 +330,20 @@ class DemoGroup < Inferno::TestGroup test 'read from scratch' do run { assert scratch[:abc] == 'xyz' } end + + test 'tag a request' do + run do + fhir_read :patient, patient_id, client: :this_client_name, tags: ['example_tag_1', 'example_tag_2'] + end + end + + test 'load a tagged request' do + run do + tagged_requests = load_tagged_requests('example_tag_1', 'example_tag_2') + + assert tagged_requests.length == 1, 'Incorrect number of requests loaded' + assert request.id == tagged_requests.first.id, 'Incorrect request loaded' + end + end end end diff --git a/lib/inferno/db/migrations/009_add_request_tags.rb b/lib/inferno/db/migrations/009_add_request_tags.rb new file mode 100644 index 000000000..cbaf6c2f9 --- /dev/null +++ b/lib/inferno/db/migrations/009_add_request_tags.rb @@ -0,0 +1,18 @@ +Sequel.migration do + change do + create_table :tags do + column :id, String, primary_key: true, null: false, size: 36 + column :name, String, size: 255, null: false + + index :name, unique: true + end + + create_table :requests_tags do + foreign_key :tags_id, :tags, index: true, type: String, null: false, size: 36, key: [:id] + foreign_key :requests_id, :requests, index: true, type: Integer, null: false, key: [:index] + + index [:tags_id, :requests_id], unique: true + index [:requests_id, :tags_id], unique: true + end + end +end diff --git a/lib/inferno/db/schema.rb b/lib/inferno/db/schema.rb index 9a25c25cc..324dd96cb 100644 --- a/lib/inferno/db/schema.rb +++ b/lib/inferno/db/schema.rb @@ -4,6 +4,15 @@ Integer :version, :default=>0, :null=>false end + create_table(:tags, :ignore_index_errors=>true) do + String :id, :size=>36, :null=>false + String :name, :size=>255, :null=>false + + primary_key [:id] + + index [:name], :unique=>true + end + create_table(:test_sessions) do String :id, :size=>36, :null=>false String :test_suite_id, :size=>255, :null=>false @@ -121,5 +130,15 @@ index [:requests_id] index [:results_id] end + + create_table(:requests_tags, :ignore_index_errors=>true) do + foreign_key :tags_id, :tags, :type=>String, :size=>36, :null=>false, :key=>[:id] + foreign_key :requests_id, :requests, :null=>false, :key=>[:index] + + index [:requests_id] + index [:requests_id, :tags_id], :unique=>true + index [:tags_id] + index [:tags_id, :requests_id], :unique=>true + end end end diff --git a/lib/inferno/dsl/fhir_client.rb b/lib/inferno/dsl/fhir_client.rb index 2f7cf93e4..7f3e4c317 100644 --- a/lib/inferno/dsl/fhir_client.rb +++ b/lib/inferno/dsl/fhir_client.rb @@ -101,9 +101,18 @@ def body_to_path(body) # other tests # @param headers [Hash] custom headers for this operation # @param operation_method [Symbol] indicates which request type to use for the operation + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_operation(path, body: nil, client: :default, name: nil, headers: {}, operation_method: :post) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_operation( + path, + body: nil, + client: :default, + name: nil, + headers: {}, + operation_method: :post, + tags: [] + ) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do operation_headers = fhir_client(client).fhir_headers operation_headers.merge!('Content-Type' => 'application/fhir+json') if body.present? @@ -127,9 +136,10 @@ def fhir_operation(path, body: nil, client: :default, name: nil, headers: {}, op # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_get_capability_statement(client: :default, name: nil) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_get_capability_statement(client: :default, name: nil, tags: []) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do fhir_client(client).conformance_statement fhir_client(client).reply @@ -143,9 +153,10 @@ def fhir_get_capability_statement(client: :default, name: nil) # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_create(resource, client: :default, name: nil) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_create(resource, client: :default, name: nil, tags: []) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do fhir_client(client).create(resource) end @@ -159,9 +170,10 @@ def fhir_create(resource, client: :default, name: nil) # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_read(resource_type, id, client: :default, name: nil) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_read(resource_type, id, client: :default, name: nil, tags: []) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do fhir_client(client).read(fhir_class_from_resource_type(resource_type), id) end @@ -176,9 +188,10 @@ def fhir_read(resource_type, id, client: :default, name: nil) # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_vread(resource_type, id, version_id, client: :default, name: nil) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_vread(resource_type, id, version_id, client: :default, name: nil, tags: []) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do fhir_client(client).vread(fhir_class_from_resource_type(resource_type), id, version_id) end @@ -192,9 +205,10 @@ def fhir_vread(resource_type, id, version_id, client: :default, name: nil) # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_update(resource, id, client: :default, name: nil) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_update(resource, id, client: :default, name: nil, tags: []) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do fhir_client(client).update(resource, id) end @@ -209,9 +223,10 @@ def fhir_update(resource, id, client: :default, name: nil) # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_patch(resource_type, id, patchset, client: :default, name: nil) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_patch(resource_type, id, patchset, client: :default, name: nil, tags: []) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do fhir_client(client).partial_update(fhir_class_from_resource_type(resource_type), id, patchset) end @@ -225,9 +240,10 @@ def fhir_patch(resource_type, id, patchset, client: :default, name: nil) # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_history(resource_type = nil, id = nil, client: :default, name: nil) - store_request_and_refresh_token(fhir_client(client), name) do + def fhir_history(resource_type = nil, id = nil, client: :default, name: nil, tags: []) + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do if id fhir_client(client).resource_instance_history(fhir_class_from_resource_type(resource_type), id) @@ -248,8 +264,16 @@ def fhir_history(resource_type = nil, id = nil, client: :default, name: nil) # @param name [Symbol] Name for this request to allow it to be used by # other tests # @param search_method [Symbol] Use `:post` to search via POST + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_search(resource_type = nil, client: :default, params: {}, name: nil, search_method: :get) + def fhir_search( + resource_type = nil, + client: :default, + params: {}, + name: nil, + search_method: :get, + tags: [] + ) search = if search_method == :post { body: params } @@ -257,7 +281,7 @@ def fhir_search(resource_type = nil, client: :default, params: {}, name: nil, se { parameters: params } end - store_request_and_refresh_token(fhir_client(client), name) do + store_request_and_refresh_token(fhir_client(client), name, tags) do tcp_exception_handler do if resource_type fhir_client(client) @@ -276,9 +300,10 @@ def fhir_search(resource_type = nil, client: :default, params: {}, name: nil, se # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_delete(resource_type, id, client: :default, name: nil) - store_request('outgoing', name) do + def fhir_delete(resource_type, id, client: :default, name: nil, tags: []) + store_request('outgoing', name, tags) do tcp_exception_handler do fhir_client(client).destroy(fhir_class_from_resource_type(resource_type), id) end @@ -291,9 +316,10 @@ def fhir_delete(resource_type, id, client: :default, name: nil) # @param client [Symbol] # @param name [Symbol] Name for this request to allow it to be used by # other tests + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def fhir_transaction(bundle = nil, client: :default, name: nil) - store_request('outgoing', name) do + def fhir_transaction(bundle = nil, client: :default, name: nil, tags: []) + store_request('outgoing', name, tags) do tcp_exception_handler do fhir_client(client).transaction_bundle = bundle if bundle.present? fhir_client(client).end_transaction @@ -312,8 +338,8 @@ def fhir_class_from_resource_type(resource_type) # expired. It's combined with `store_request` so that all of the fhir # request methods don't have to be wrapped twice. # @private - def store_request_and_refresh_token(client, name, &block) - store_request('outgoing', name) do + def store_request_and_refresh_token(client, name, tags, &block) + store_request('outgoing', name, tags) do perform_refresh(client) if client.need_to_refresh? && client.able_to_refresh? block.call end diff --git a/lib/inferno/dsl/http_client.rb b/lib/inferno/dsl/http_client.rb index 8ed55ebae..3e399a0d0 100644 --- a/lib/inferno/dsl/http_client.rb +++ b/lib/inferno/dsl/http_client.rb @@ -68,9 +68,10 @@ def http_clients # @param name [Symbol] Name for this request to allow it to be used by # other tests # @param headers [Hash] Input headers here + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def get(url = '', client: :default, name: nil, headers: nil) - store_request('outgoing', name) do + def get(url = '', client: :default, name: nil, headers: nil, tags: []) + store_request('outgoing', name, tags) do tcp_exception_handler do client = http_client(client) @@ -103,9 +104,10 @@ def connection # @param name [Symbol] Name for this request to allow it to be used by # other tests # @param headers [Hash] Input headers here + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def post(url = '', body: nil, client: :default, name: nil, headers: nil) - store_request('outgoing', name) do + def post(url = '', body: nil, client: :default, name: nil, headers: nil, tags: []) + store_request('outgoing', name, tags) do tcp_exception_handler do client = http_client(client) @@ -129,9 +131,10 @@ def post(url = '', body: nil, client: :default, name: nil, headers: nil) # @param name [Symbol] Name for this request to allow it to be used by # other tests # @param headers [Hash] Input headers here + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def delete(url = '', client: :default, name: :nil, headers: nil) - store_request('outgoing', name) do + def delete(url = '', client: :default, name: :nil, headers: nil, tags: []) + store_request('outgoing', name, tags) do tcp_exception_handler do client = http_client(client) @@ -159,8 +162,9 @@ def delete(url = '', client: :default, name: :nil, headers: nil) # @param name [Symbol] Name for this request to allow it to be used by # other tests # @param headers [Hash] Input headers here + # @param tags [Array] a list of tags to assign to the request # @return [Inferno::Entities::Request] - def stream(block, url = '', limit = 100, client: :default, name: nil, headers: nil) + def stream(block, url = '', limit = 100, client: :default, name: nil, headers: nil, tags: []) streamed = [] collector = proc do |chunk, bytes| @@ -169,7 +173,7 @@ def stream(block, url = '', limit = 100, client: :default, name: nil, headers: n block.call(chunk, bytes) end - store_request('outgoing', name) do + store_request('outgoing', name, tags) do tcp_exception_handler do client = http_client(client) diff --git a/lib/inferno/dsl/request_storage.rb b/lib/inferno/dsl/request_storage.rb index 14e4e938c..73fb648c2 100644 --- a/lib/inferno/dsl/request_storage.rb +++ b/lib/inferno/dsl/request_storage.rb @@ -36,24 +36,36 @@ def resource request&.resource end + # Returns requests which match all of the given tags + # + # @param tags [String] + # @return [Inferno::Entities::Request] + def load_tagged_requests(*tags) + return [] if tags.blank? + + Repositories::Requests.new.tagged_requests(test_session_id, tags).tap do |tagged_requests| + requests.concat(tagged_requests) + end + end + # @private def named_request(name) requests.find { |request| request.name == self.class.config.request_name(name.to_sym) } end # @private - def store_request(direction, name = nil, &block) + def store_request(direction, name, tags, &block) response = block.call name = self.class.config.request_name(name) request = if response.is_a? FHIR::ClientReply Entities::Request.from_fhir_client_reply( - response, direction:, name:, test_session_id: + response, direction:, name:, test_session_id:, tags: ) else Entities::Request.from_http_response( - response, direction:, name:, test_session_id: + response, direction:, name:, test_session_id:, tags: ) end diff --git a/lib/inferno/dsl/resume_test_route.rb b/lib/inferno/dsl/resume_test_route.rb index e0ff972bb..211ae24b6 100644 --- a/lib/inferno/dsl/resume_test_route.rb +++ b/lib/inferno/dsl/resume_test_route.rb @@ -23,6 +23,11 @@ def test_run_identifier_block self.class.singleton_class.instance_variable_get(:@test_run_identifier_block) end + # @private + def tags + self.class.singleton_class.instance_variable_get(:@tags) + end + # @private def find_test_run(test_run_identifier) test_runs_repo.find_latest_waiting_by_identifier(test_run_identifier) @@ -44,7 +49,8 @@ def persist_request(request, test_run, waiting_result, test) request.to_hash.merge( test_session_id: test_run.test_session_id, result_id: waiting_result.id, - name: test.config.request_name(test.incoming_request_name) + name: test.config.request_name(test.incoming_request_name), + tags: ) ) end diff --git a/lib/inferno/dsl/runnable.rb b/lib/inferno/dsl/runnable.rb index 968ead399..12b46691a 100644 --- a/lib/inferno/dsl/runnable.rb +++ b/lib/inferno/dsl/runnable.rb @@ -318,7 +318,7 @@ def suite # # @see Inferno::DSL::Results#wait # @example - # resume_test_route :get, '/launch' do + # resume_test_route :get, '/launch', tags: ['launch'] do # request.query_parameters['iss'] # end # @@ -341,15 +341,17 @@ def suite # [Any of the path options available in Hanami # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl) # can be used here. + # @param tags [Array] a list of tags to assign to the request # @yield This method takes a block which must return the identifier # defined when a test was set to wait for the test run that hit this # route. The block has access to the `request` method which returns a # {Inferno::Entities::Request} object with the information for the # incoming request. # @return [void] - def resume_test_route(method, path, &block) + def resume_test_route(method, path, tags: [], &block) route_class = Class.new(ResumeTestRoute) do |klass| klass.singleton_class.instance_variable_set(:@test_run_identifier_block, block) + klass.singleton_class.instance_variable_set(:@tags, tags) end route(method, path, route_class) diff --git a/lib/inferno/entities/request.rb b/lib/inferno/entities/request.rb index 9ea0a6907..60409d16f 100644 --- a/lib/inferno/entities/request.rb +++ b/lib/inferno/entities/request.rb @@ -34,7 +34,7 @@ class Request < Entity ATTRIBUTES = [ :id, :index, :verb, :url, :direction, :name, :status, :request_body, :response_body, :result_id, :test_session_id, :created_at, - :updated_at, :headers + :updated_at, :headers, :tags ].freeze SUMMARY_FIELDS = [ :id, :index, :url, :verb, :direction, :name, :status, :result_id, :created_at, :updated_at @@ -48,6 +48,18 @@ def initialize(params) @name = params[:name]&.to_sym @headers = params[:headers]&.map { |header| header.is_a?(Hash) ? Header.new(header) : header } || [] + format_tags(params[:tags] || []) + end + + def format_tags(raw_tags) + @tags = raw_tags.map do |tag| + case tag + when Hash + tag[:name] + when String + tag + end + end end # @return [Hash] @@ -124,6 +136,7 @@ def to_hash test_session_id:, request_headers: request_headers.map(&:to_hash), response_headers: response_headers.map(&:to_hash), + tags:, created_at:, updated_at: }.compact @@ -138,7 +151,7 @@ def resource class << self # @private - def from_hanami_request(request, name: nil) + def from_hanami_request(request, name: nil, tags: []) url = "#{request.base_url}#{request.path}" url += "?#{request.query_string}" if request.query_string.present? request_headers = @@ -153,12 +166,13 @@ def from_hanami_request(request, name: nil) direction: 'incoming', name:, request_body: request.body.string, - headers: request_headers + headers: request_headers, + tags: ) end # @private - def from_http_response(response, test_session_id:, direction: 'outgoing', name: nil) + def from_http_response(response, test_session_id:, direction: 'outgoing', name: nil, tags: []) request_headers = response.env.request_headers .map { |header_name, value| Header.new(name: header_name.downcase, value:, type: 'request') } @@ -175,12 +189,13 @@ def from_http_response(response, test_session_id:, direction: 'outgoing', name: request_body: response.env.request_body, response_body: response.body, test_session_id:, - headers: request_headers + response_headers + headers: request_headers + response_headers, + tags: ) end # @private - def from_fhir_client_reply(reply, test_session_id:, direction: 'outgoing', name: nil) + def from_fhir_client_reply(reply, test_session_id:, direction: 'outgoing', name: nil, tags: []) request = reply.request response = reply.response request_headers = request[:headers] @@ -203,7 +218,8 @@ def from_fhir_client_reply(reply, test_session_id:, direction: 'outgoing', name: request_body:, response_body: response[:body], test_session_id:, - headers: request_headers + response_headers + headers: request_headers + response_headers, + tags: ) end end diff --git a/lib/inferno/repositories/requests.rb b/lib/inferno/repositories/requests.rb index b70a32126..56d745ce6 100644 --- a/lib/inferno/repositories/requests.rb +++ b/lib/inferno/repositories/requests.rb @@ -1,19 +1,19 @@ module Inferno module Repositories class Requests < Repository - include Import[headers_repo: 'inferno.repositories.headers'] + include Import[ + headers_repo: 'inferno.repositories.headers', + tags_repo: 'inferno.repositories.tags' + ] def create(params) request = self.class::Model.create(db_params(params)) - request_headers = (params[:request_headers] || []).map do |header| - request.add_header(header.merge(request_id: request.index, type: 'request')) - end - response_headers = (params[:response_headers] || []).map do |header| - request.add_header(header.merge(request_id: request.index, type: 'response')) - end + headers = create_headers(request, params) - headers = (request_headers + response_headers).map { |header| headers_repo.build_entity(header.to_hash) } + params[:tags]&.each do |tag| + request.add_tag(tag) + end build_entity( request.to_hash @@ -22,6 +22,17 @@ def create(params) ) end + def create_headers(request, params) + request_headers = (params[:request_headers] || []).map do |header| + request.add_header(header.merge(request_id: request.index, type: 'request')) + end + response_headers = (params[:response_headers] || []).map do |header| + request.add_header(header.merge(request_id: request.index, type: 'response')) + end + + (request_headers + response_headers).map { |header| headers_repo.build_entity(header.to_hash) } + end + def find(id) result = self.class::Model @@ -56,6 +67,19 @@ def find_named_request(test_session_id, name) build_entity(result) end + def tagged_requests(test_session_id, tags) + self.class::Model + .tagged_requests(test_session_id, tags) + .to_a + .map! do |request| + build_entity( + request + .to_json_data(json_serializer_options) + .deep_symbolize_keys! + ) + end + end + def requests_for_result(result_id) self.class::Model .order(:index) @@ -68,14 +92,25 @@ def requests_for_result(result_id) def json_serializer_options { - include: :headers + include: [:headers, :tags] } end class Model < Sequel::Model(db) - many_to_many :result, class: 'Inferno::Repositories::Results::Model', join_table: :requests_results, - left_key: :request_id, right_key: :result_id - one_to_many :headers, class: 'Inferno::Repositories::Headers::Model', key: :request_id + many_to_many :result, + class: 'Inferno::Repositories::Results::Model', + join_table: :requests_results, + left_key: :request_id, + right_key: :result_id + many_to_many :tags, + class: 'Inferno::Repositories::Tags::Model', + join_table: :requests_tags, + left_key: :requests_id, + right_key: :tags_id, + adder: :add_tag + one_to_many :headers, + class: 'Inferno::Repositories::Headers::Model', + key: :request_id def before_create self.id = SecureRandom.uuid @@ -84,6 +119,57 @@ def before_create self.updated_at ||= time super end + + def add_tag(tag_name) + tag = Tags::Model.find_or_create(name: tag_name) + + Inferno::Application['db.connection'][:requests_tags].insert( + tags_id: tag.id, + requests_id: index + ) + end + + def self.tagged_requests_sql + # Find all the requests for the current session which: + # - match all supplied tags + # - are the from the most recent test run for each runnable + <<~SQL.gsub(/\s+/, ' ').freeze + select final_requests.* + from ( + select uncounted_requests.request_id request_id + from ( + select r.id request_id, t.id tag_id from requests r + inner join requests_tags rt on r.`index` = rt.requests_id + inner join tags t on rt.tags_id = t.id + where r.test_session_id = :test_session_id + and r.result_id in ( + SELECT a.id FROM results a + WHERE a.test_session_id = r.test_session_id + AND a.id IN ( + SELECT id + FROM results b + WHERE (b.test_session_id = a.test_session_id AND b.test_id = a.test_id) OR + (b.test_session_id = a.test_session_id AND b.test_group_id = a.test_group_id) OR + (b.test_session_id = a.test_session_id AND b.test_suite_id = a.test_suite_id) + ORDER BY updated_at DESC + LIMIT 1 + ) + ) + and t.name in :tags + group by r.id, t.id + ) as uncounted_requests + group by uncounted_requests.request_id + having count(*) = :tag_count + ) as matched_requests + inner join requests final_requests on final_requests.id = matched_requests.request_id + where final_requests.test_session_id = :test_session_id + order by final_requests.`index` + SQL + end + + def self.tagged_requests(test_session_id, tags) + fetch(tagged_requests_sql, test_session_id:, tags:, tag_count: tags.length) + end end end end diff --git a/lib/inferno/repositories/tags.rb b/lib/inferno/repositories/tags.rb new file mode 100644 index 000000000..1f7a44247 --- /dev/null +++ b/lib/inferno/repositories/tags.rb @@ -0,0 +1,18 @@ +module Inferno + module Repositories + class Tags < Repository + class Model < Sequel::Model(db) + many_to_many :requests, + class: 'Inferno::Repositories::Requests::Model', + join_table: :requests_tags, + left_key: :tags_id, + right_key: :requests_id + + def before_create + self.id = SecureRandom.uuid + super + end + end + end + end +end diff --git a/spec/factories/request.rb b/spec/factories/request.rb index dfece6edb..22d1b9fe7 100644 --- a/spec/factories/request.rb +++ b/spec/factories/request.rb @@ -27,6 +27,8 @@ ] end + tags { [] } + request_body { nil } sequence(:response_body) { |n| "RESPONSE_BODY #{n}" } diff --git a/spec/inferno/dsl/fhir_client_spec.rb b/spec/inferno/dsl/fhir_client_spec.rb index 65a5c1834..6c860aba2 100644 --- a/spec/inferno/dsl/fhir_client_spec.rb +++ b/spec/inferno/dsl/fhir_client_spec.rb @@ -387,6 +387,13 @@ def test_session_id expect(group.request).to eq(result) end + it 'adds tags to the request' do + tags = ['abc', 'def'] + request = group.fhir_get_capability_statement(tags:) + + expect(request.tags).to match_array(tags) + end + context 'with the client parameter' do it 'uses that client' do other_url = 'http://www.example.com/fhir/r4' diff --git a/spec/inferno/dsl/http_client_spec.rb b/spec/inferno/dsl/http_client_spec.rb index 0f49cac68..6729b6c06 100644 --- a/spec/inferno/dsl/http_client_spec.rb +++ b/spec/inferno/dsl/http_client_spec.rb @@ -122,6 +122,17 @@ def setup_default_client expect(request.response_body).to eq('BODY') end + it 'adds tags to a request' do + stub_request(:get, base_url) + .to_return(status: 200, body: response_body) + + tags = ['abc', 'def'] + + request = group.get(tags:) + + expect(request.tags).to match_array(tags) + end + context 'without a url argument' do let(:stub_get_request) do stub_request(:get, base_url) diff --git a/spec/inferno/repositories/requests_spec.rb b/spec/inferno/repositories/requests_spec.rb index 0f98d1e19..700106c6c 100644 --- a/spec/inferno/repositories/requests_spec.rb +++ b/spec/inferno/repositories/requests_spec.rb @@ -5,23 +5,23 @@ let(:test_run) { repo_create(:test_run) } let(:result) { repo_create(:result, test_run:) } let(:test_session) { test_run.test_session } + let(:request_params) do + { + verb: 'get', + url: 'http://example.com', + direction: 'outgoing', + status: 200, + request_body: 'REQUEST_BODY', + response_body: 'RESPONSE_BODY', + result_id: result.id, + test_session_id: test_session.id, + request_headers: [{ name: 'REQUEST_HEADER_NAME', value: 'REQUEST_HEADER_VALUE', type: 'request' }], + response_headers: [{ name: 'RESPONSE_HEADER_NAME', value: 'RESPONSE_HEADER_VALUE', type: 'response' }], + tags: ['abc', 'def'] + } + end describe '#create' do - let(:request_params) do - { - verb: 'get', - url: 'http://example.com', - direction: 'outgoing', - status: 200, - request_body: 'REQUEST_BODY', - response_body: 'RESPONSE_BODY', - result_id: result.id, - test_session_id: test_session.id, - request_headers: [{ name: 'REQUEST_HEADER_NAME', value: 'REQUEST_HEADER_VALUE', type: 'request' }], - response_headers: [{ name: 'RESPONSE_HEADER_NAME', value: 'RESPONSE_HEADER_VALUE', type: 'response' }] - } - end - it 'persists a request' do request = repo.create(request_params) @@ -67,7 +67,7 @@ end describe '#find_full_request' do - let(:persisted_request) { repo_create(:request) } + let(:persisted_request) { repo_create(:request, request_params) } it 'returns a complete request' do request = repo.find_full_request(persisted_request.id) @@ -86,6 +86,8 @@ expect(request.headers.length).to eq(persisted_request.headers.length) expect(request.request_headers.length).to eq(persisted_request.request_headers.length) expect(request.response_headers.length).to eq(persisted_request.response_headers.length) + expect(request.tags).to be_present + expect(request.tags).to match_array(persisted_request.tags) end end @@ -127,4 +129,52 @@ expect(request.headers).to be_present end end + + describe '#tagged_requests' do + let(:request) { repo_create(:request, request_params) } + + it 'returns an empty array when no requests match' do + requests = repo.tagged_requests(request.test_session_id, [SecureRandom.uuid]) + + expect(requests).to be_an(Array) + expect(requests.length).to eq(0) + end + + it 'returns requests matching a tag' do + tags = request.tags + + tags.each do |tag| + requests = repo.tagged_requests(request.test_session_id, [tag]) + + expect(requests.length).to eq(1) + expect(requests.first.id).to eq(request.id) + end + end + + it 'returns requests matching all tags' do + requests = repo.tagged_requests(request.test_session_id, request.tags) + + expect(requests.length).to eq(1) + expect(requests.first.id).to eq(request.id) + end + + it 'only returns requests from the most recent result for a runnable' do + new_request = + repo_create( + :request, + test_session_id: request.test_session_id, + result: repo_create( + :result, + test_session_id: request.test_session_id, + updated_at: Time.now + 1.minute + ), + tags: request.tags + ) + + requests = repo.tagged_requests(request.test_session_id, request.tags) + + expect(requests.length).to eq(1) + expect(requests.first.id).to eq(new_request.id) + end + end end