# typed: false require 'active_support/core_ext/class/subclasses' require 'active_support/inflector/methods' require 'faraday' module Setsuzoku module DynamicSpecHelper ### EXTERNAL PLUGIN SPEC HELPERS ### # # These specs are designed to test and ensure that a plugin's implementations uphold their contract. # Additionally they ensure that a plugin's blackbox stubbing also upholds their contract. # # # This will register plugin specs for a base plugin class that defines its interfaces. # # It will register specs to ensure the base plugin's black box stubs are correct. # # It will also register specs for all implementations of the plugin to ensure the implementations are valid. # (This is similar to register_plugin_tests_and_stubs) def register_all_plugin_tests_and_stubs(plugin_class: self.described_class, subclasses_to_register: plugin_class.subclasses, registering_instance: nil, stub_directory: nil) context "*Dynamic Specs* for #{plugin_class.name}: Stub Definitions" do let(:plugin){ plugin_class.new } plugin_class.spec_definitions.each do |action_name, spec| define_stub_plugin_method(plugin_class, action_name, spec) it(spec[:name], action_name: action_name, &spec[:spec]) end end perform_register(plugin_class, registering_instance: registering_instance, subclasses_to_register: subclasses_to_register, stub_directory: stub_directory) end # # This will register plugin specs for a single plugin class. # The specs will test the plugin to ensure its implementations are valid. def register_plugin_tests_and_stubs(plugin_class, registering_instance: nil, stub_directory: nil) perform_register(plugin_class, registering_instance: registering_instance, stub_directory: stub_directory) end # ### END EXTERNAL PLUGIN SPEC HELPERS ### ### APPLICATION SPEC HELPERS ### # # These stubs completely black box the plugin's functionality and returns a stubbed response # without invoking the plugin's method. # # Stub all plugin interface methods at a context level for an application class that registers a plugin. # This also defines a before each to execute the stubs. # # @param registering_class [Array/Class] the application's class/es that register a plugin and need methods stubbed. # # @return void def stub_all_plugin_methods(registering_class = self.described_class) registering_class = [registering_class] unless registering_class.is_a?(Array) registering_class.each do |klass| plugin_class = klass.plugin_context[:plugin_class] plugin_class.spec_definitions.each do |action_name, spec| define_stub_plugin_method(plugin_class, action_name, spec, true) end end end # # Stub all plugin interface methods for an application class that registers a plugin for a single spec. # # @param registering_class [Array/Class] the application's class/es that register a plugin and need methods stubbed. # # @return void def stub_plugin_methods_for_spec(registering_class = self.described_class) plugin_class = registering_class.plugin_context[:plugin_class] plugin_class.spec_definitions.each do |action_name, spec| plugin_class.any_instance.stub(action_name).and_return(spec[:stub][:response]) # a plugin spec might have some dynamic data, e.g. certain methods are called with plugin specific args. if spec[:stub][:dynamic_methods] spec[:stub][:dynamic_methods].each do |meth, resp| #how can we ignore the call count instead of just putting a high limit? plugin_class.any_instance.stub(meth).and_return(resp) end end end end # ### END APPLICATION SPEC HELPERS ### private def perform_register(plugin_class, register_tests = true, registering_instance: nil, subclasses_to_register: nil, stub_directory: nil) #iterate over all api methods defined by the plugin and create a context and tests for each method # in register_tests_and_get_stubs, and return the stub defs up to this method. plugin_classes = subclasses_to_register || [plugin_class] plugin_classes.each do |plugin_class| p = plugin_class.new(registering_instance: registering_instance) register_tests_and_get_stubs(p, register_tests).each do |method_name, stubs| register_stub(p, method_name, stubs, registering_instance: registering_instance, stub_directory: stub_directory) end end end # This will iterate over every registered plugin defined in the including helper # and generate formatted hashes of stub configs. # # register_stubs uses these config objects to define stubs dynamically def register_tests_and_get_stubs(plugin, register_tests = true) definitions = {} specs = [] action_name_namespace = :"#{plugin.name.gsub(' ', '').underscore}" stub_name = :"stub_#{action_name_namespace}" definitions[stub_name] = { action_name_namespace => {} } plugin.api_actions.each do |api_action| action_name, options = api_action plugin.api_strategy.current_action = action_name request_props = plugin.api_strategy.get_request_properties(action_name: action_name, for_stub: true) (definitions[stub_name][action_name_namespace][action_name] ||= []) << { method: request_props[:request_method], request_format: request_props[:request_format], response_format: request_props[:response_format], url: request_props[:formatted_full_url], stub_data: request_props[:stub_data] } # include the generic interface specs for this plugin's action specs << { spec: plugin.class.spec_definitions[action_name], action_name: action_name } end if register_tests context "*Dynamic Specs* for #{plugin.class.name}: Interface Implementations" do #we will instantiate the plugin, any registered_instance dependent methods/data must be stubbed by the plugin because of this! let!(:plugin) { plugin } before :each do send(stub_name, self) end specs.each do |spec_def| if spec_def[:spec] && spec_def[:spec][:spec] it(spec_def[:spec][:name], &spec_def[:spec][:spec]) else it "Undefined spec for #{spec_def[:action_name]}" do raise NotImplemented, "Spec for action: #{spec_def[:action_name]} is not defined. Expected #{plugin.class} or its parent to define it." end end end end end definitions end # This method will then iterate over stub defs and actually define the stubs here in the stub_defs array. # It will then lastly collect all the stubs in stub_defs array and do call to define the stub. def register_stub(plugin, method_name, stubs, registering_instance: nil, stub_directory: nil) stub_defs = [] stubs.each do |provider, stub| stub.each do |action_name, props| props.each do |propz| plugin.api_strategy.current_action = action_name default_headers = plugin.api_strategy.request_options(propz[:request_format])[:headers] || { 'Content-Type' => "application/#{propz[:request_format]}" } if plugin.auth_strategy.is_a?(Setsuzoku::Service::WebService::AuthStrategies::BasicAuthStrategy) basic_auth = plugin.api_strategy.request_options(propz[:request_format])[:basic_auth] auth_header = { 'Authorization' => "Basic #{Base64.encode64("#{basic_auth[:username]}:#{basic_auth[:password]}").gsub("\n", '')}" } elsif plugin.auth_strategy.is_a?(Setsuzoku::Service::WebService::AuthStrategies::OAuthStrategy) auth_header = { 'Authorization' => "Bearer #{plugin.api_strategy.request_options(propz[:request_format])[:token]}" } end default_headers.merge!(auth_header) if auth_header headers = propz[:headers] ? [propz[:headers]] : [default_headers] stub_directory ||= "#{plugin.class.plugin_namespace}/#{provider}" headers.each do |header| header = Faraday.new.headers .merge({'Accept'=>'*/*','Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'}) .merge(header.deep_stringify_keys) if propz[:stub_data] # we have specified specific stub data for this action propz[:stub_data].each do |stub_datum| url, body = get_url_and_body(plugin, propz, stub_directory, action_name, stub_datum[:url_params]) stub_defs << proc do stub_request(propz[:method], url) .with( headers: header, body: body, query: stub_datum[:url_params] ) .to_return(status: 200, headers: {}, body: File.read("#{Dir.pwd}/spec/support/setsuzoku/#{plugin.class.plugin_namespace}/#{provider}/#{action_name}.#{propz[:response_format]}")) end end else # this is a generic action so the stub uses the default structure url, body = get_url_and_body(plugin, propz, stub_directory, action_name) stub_defs << proc do stub_request(propz[:method], url) .with( headers: header, body: body ) .to_return(status: 200, headers: {}, body: File.read("#{Dir.pwd}/spec/support/setsuzoku/#{plugin.class.plugin_namespace}/#{provider}/#{action_name}.#{propz[:response_format]}")) end end end end end end #iterate over all the stub_requests collected above and call them to stub them. define_method method_name do |spec_instance = self| stub_defs.each do |stub_def| instance_exec(&stub_def) end end end def define_stub_plugin_method(plugin_class, action_name, spec, always_stub = false) stub_plugin_method_name = :"stub_plugin_method_#{action_name}" define_method stub_plugin_method_name do #TODO: let this use the appropriate rspec instance stub method #TODO: Does it make sense to have the stub with the method? or should it be in a json file we parse? plugin_class.any_instance.stub(action_name).and_return(spec[:stub][:response]) # a plugin spec might have some dynamic data, e.g. certain methods are called with plugin specific args. if spec[:stub][:dynamic_methods] spec[:stub][:dynamic_methods].each do |meth, resp| #how can we ignore the call count instead of just putting a high limit? plugin_class.any_instance.stub(meth).and_return(resp) end end end if always_stub before :each do send(stub_plugin_method_name) end else before :each, action_name: action_name do send(stub_plugin_method_name) end end end def get_url_and_body(plugin, propz, stub_directory, action_name, url_params = nil) body = nil url = propz[:url] format = propz[:request_format].to_s.include?('x-www-form') || propz[:request_format] == :file ? :json : propz[:request_format] begin body = File.read("#{Dir.pwd}/spec/support/setsuzoku/#{stub_directory}/#{action_name}_request.#{format}") body.squish! case format when :xml body.gsub!('> <', '><') when :json body.gsub!(' "', '"') body.gsub!('" ', '"') body.gsub!(': ', ':') body.gsub!('{ ', '{') body.gsub!(' }', '}') end # We will convert these to url params when making requests. need to replicate that. req_params = case format when :json temp_body = JSON.parse(body) if url_params url_params.each{ |k,v| temp_body.delete(k.to_s) } end temp_body else # will we need to parse xml here? req_params = {} body end if url.match?(/.*\{\{.*\}\}/) url, body = plugin.api_strategy.replace_dynamic_vars(full_url: url, req_params: req_params) end if propz[:request_format] == :file body = body.to_json if body.is_a?(Hash) elsif format == :json #faraday sorts hash keys for some reason body = body.to_json if body.is_a?(Hash) body = JSON.parse(body).sort.to_h end [url, body] rescue [url, nil] end end end end