# frozen_string_literal: true # @TODO: replace hashie! require 'hashie' module RopenPi module Specs # rubocop:disable Metrics/ModuleLength module ExampleGroupHelpers def path(template, metadata = {}, &block) metadata[:path_item] = { template: template } describe(template, metadata, &block) end %i[get post patch put delete head].each do |verb| define_method(verb) do |summary, &block| api_metadata = { operation: { verb: verb, summary: summary } } describe(verb, api_metadata, &block) end end %i[operationId deprecated security].each do |attr_name| define_method(attr_name) do |value| metadata[:operation][attr_name] = value end end # NOTE: 'description' requires special treatment because ExampleGroup already # defines a method with that name. Provide an override that supports the existing # functionality while also setting the appropriate metadata if applicable def description(value = nil) return super() if value.nil? metadata[:operation][:description] = value end # These are array properties - note the splat operator %i[tags consumes produces schemes].each do |attr_name| define_method(attr_name) do |*value| metadata[:operation][attr_name] = value end end def request_body(attributes) # can make this generic, and accept any incoming hash (like parameter method) attributes.compact! if metadata[:operation][:requestBody].blank? metadata[:operation][:requestBody] = attributes elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content] # merge in content_hash = metadata[:operation][:requestBody][:content] incoming_content_hash = attributes[:content] content_hash.merge!(incoming_content_hash) if incoming_content_hash end end def request_body_json(schema:, required: true, description: nil, examples: nil) passed_examples = Array(examples) content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} } request_body(description: description, required: required, content: content_hash) handle_examples(schema: schema, examples: passed_examples, required: required) if passed_examples.any? end # rubocop:disable Metrics/MethodLength def handle_examples(schema:, examples:, required:) # the request_factory is going to have to resolve the different ways that the example can be given # it can contain a 'value' key which is a direct hash (easiest) # it can contain a 'external_value' key which makes an external call to load the json # it can contain a '$ref' key. Which points to #/components/examples/blog # rubocop:disable Metrics/BlockLength examples.each do |passed_example| param_attributes = if passed_example.is_a?(Symbol) example_key_name = passed_example # TODO: write more tests around this adding to the parameter # if symbol try and use save_request_example { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema } elsif passed_example.is_a?(Hash) && passed_example[:externalValue] { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema } elsif passed_example.is_a?(Hash) && passed_example['$ref'] { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema } end parameter(param_attributes) end # rubocop:enable Metrics/BlockLength end # rubocop:enable Metrics/MethodLength def request_body_text_plain(required: false, description: nil, examples: nil) content_hash = { 'test/plain' => { schema: { type: :string }, examples: examples }.compact! || {} } request_body(description: description, required: required, content: content_hash) end # TODO: add examples to this like we can for json, might be large lift # as many assumptions are made on content-type def request_body_xml(schema:, required: false, description: nil, examples: nil) # passed_examples = Array(examples) content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} } request_body(description: description, required: required, content: content_hash) end # rubocop:disable Metrics/MethodLength def request_body_multipart(schema:, description: nil) content_hash = { 'multipart/form-data' => { schema: schema } } request_body(description: description, content: content_hash) schema.extend(Hashie::Extensions::DeepLocate) file_properties = schema.deep_locate ->(_k, v, _obj) { v == :binary } hash_locator = [] file_properties.each do |match| hash_match = schema.deep_locate ->(_k, v, _obj) { v == match } hash_locator.concat(hash_match) unless hash_match.empty? end property_hashes = hash_locator.flat_map do |locator| locator.select { |_k, v| file_properties.include?(v) } end existing_keys = [] property_hashes.each do |property_hash| property_hash.keys.each do |k| next if existing_keys.include?(k) file_name = k existing_keys << k parameter name: file_name, in: :formData, type: :file, required: true end end end # rubocop:enable Metrics/MethodLength def parameter(attributes) attributes[:required] = true if attributes[:in] && attributes[:in].to_sym == :path attributes[:schema] = { type: attributes[:type] } if attributes[:type] && attributes[:schema].nil? if metadata.key?(:operation) metadata[:operation][:parameters] ||= [] metadata[:operation][:parameters] << attributes else metadata[:path_item][:parameters] ||= [] metadata[:path_item][:parameters] << attributes end end def response(code, description, metadata = {}, &block) metadata[:response] = { code: code, description: description } context(description, metadata, &block) end def schema(value, content_type: 'application/json') content_hash = { content_type => { schema: value } } metadata[:response][:content] = content_hash end def header(name, attributes) metadata[:response][:headers] ||= {} if attributes[:type] && attributes[:schema].nil? attributes[:schema] = { type: attributes[:type] } attributes.delete(:type) end metadata[:response][:headers][name] = attributes end # NOTE: Similar to 'description', 'examples' need to handle the case when # being invoked with no params to avoid overriding 'examples' method of # rspec-core ExampleGroup def examples(example = nil) return super() if example.nil? metadata[:response][:examples] = example end # checks the examples in the parameters should be able to add $ref and externalValue examples. # This syntax would look something like this in the integration _spec.rb file # # request_body_json schema: { '$ref' => '#/components/schemas/blog' }, # examples: [:blog, {name: :external_blog, # externalValue: 'http://api.sample.org/myjson_example'}, # {name: :another_example, # '$ref' => '#/components/examples/flexible_blog_example'}] # The first value :blog, points to a let param of the same name, and is used to make the request in the # integration test (it is used to build the request payload) # # The second item in the array shows how to add an externalValue for the examples in the requestBody section # The third item shows how to add a $ref item that points to the components/examples section of the swagger spec. # # NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui # will not show it yet def merge_other_examples!(example_metadata) # example.metadata[:operation][:requestBody][:content]['application/json'][:examples] content_node = example_metadata[:operation][:requestBody][:content]['application/json'] return unless content_node external_example = example_metadata[:operation]&.dig(:parameters)&.detect do |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] end || {} ref_example = example_metadata[:operation]&.dig(:parameters)&.detect do |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] end || {} examples_node = content_node[:examples] ||= {} nodes_to_add = [] nodes_to_add << external_example unless external_example.empty? nodes_to_add << ref_example unless ref_example.empty? nodes_to_add.each do |node| json_request_examples = examples_node ||= {} other_name = node[:name][:name] other_key = node[:name][:externalValue] ? :externalValue : '$ref' json_request_examples.merge!(other_name => { other_key => node[:param_value] }) if other_name end end def run_test!(&block) app_json = 'application/json' before do |example| submit_request(example.metadata) end it "returns a #{metadata[:response][:code]} response" do |example| assert_response_matches_metadata(example.metadata, &block) example.instance_exec(response, &block) if block_given? end after do |example| body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect do |p| p[:in] == :body && p[:required] end if body_parameter && respond_to?(body_parameter[:name]) && \ example.metadata[:operation][:requestBody][:content][app_json] # save response examples by default if example.metadata[:response][:examples].nil? || example.metadata[:response][:examples].empty? unless response.body.to_s.empty? example.metadata[:response][:examples] = { app_json => JSON.parse(response.body, symbolize_names: true) } end end # save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test if response.code.to_s =~ /^2\d{2}$/ example.metadata[:operation][:requestBody][:content][app_json] = { examples: {} } \ unless example.metadata[:operation][:requestBody][:content][app_json][:examples] json_request_examples = example.metadata[:operation][:requestBody][:content][app_json][:examples] json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) } example.metadata[:operation][:requestBody][:content][app_json][:examples] = json_request_examples end end self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody] end end end # rubocop:enable Metrics/ModuleLength end end