# FIXME: Is there an easier way to load these other than just force feeding the filenames? Rtml::Test::SimulatorPostProcessors::CardParsers Rtml::Test::SimulatorPostProcessors::Submit Rtml::Test::SimulatorPostProcessors::Receipt class Rtml::Test::Simulator delegate :response, :request, :controller, :to => :session delegate :screens, :current_screen, :current_screen_id, :next_screen, :next_screen_id, :waiting_for_input?, :find_screen, :process, :continue, :receipt, :to => :app attr_reader :app, :session # Accepts a path to the TML document which should be requested. To pass raw TML, # use this syntax: # Rtml::Test::Simulator.new(:tml => my_tml_code) def initialize(start_at = nil) @session = ActionController::Integration::Session.new @variables = Rtml::Test::VariableScope.new @headers = HashWithIndifferentAccess.new if start_at.kind_of?(Hash) load_tml! start_at[:tml] else visit(start_at) unless start_at.nil? end end def post_data(location, data = {}) post(location, data, @headers) load_tml!(response.body) end def header(name, value) @headers[name] = value end def headers @headers.dup end # Instead of using "visit", this just force-feeds some TML into the simulator. Useful for # hard-coded, quick-and-dirty debug situations, but far less useful for testing a real app # or a large document. def load_tml!(tml) @path = nil build_app(tml) process end def process(forward = false, &block) # FIXME: TODO: Refactor, this is too ugly! if block_given? uri = catch(:new_document) { yield; nil } else uri = catch(:new_document) do if forward app.continue_forward else app.continue end nil end end if uri if uri =~ /^tmlvar\:(.*)$/ # it's a TML variable referencing a screen ID uri = variables[$~[1]] if uri.blank? raise Rtml::Errors::SimulationError, "Empty screen reference: tmlvar #{$~[1]} on screen #{current_screen_id}" end end if uri == @path && current_screen_id == app.screens.first['id'] raise Rtml::Errors::SimulationError, "Circular foreign reference: #{@path}##{current_screen_id}" end visit_uri(uri) end do_post_processing! end def visit(path, options = {}) options.reverse_merge! default_options path = "##{path}" if path.kind_of?(Symbol) if File.file?(path) build_app File.read(@path = path) else path, screen = path_and_screen_from path if !path.blank? && @path != path get_and_build(path) else app.screenflow.clear end if screen && !screen.blank? && screen != '#' app.jump_to_screen screen end end process if options[:process] end # Visits the specified URI. # This merely wraps a call to #visit, but is called internally by #process when following # a direction to visit a new URI outside of the current document. This makes it useful for # setting up test expectations because you can make sure that #visit_uri is (or isn't) # called without worrying about breaking #visit. def visit_uri(uri) visit(uri) end def follow_link(path_or_text) on_current_screen("a").each do |hyperlink| if hyperlink['href'] == path_or_text || hyperlink.inner_text =~ /#{Regexp::escape path_or_text}/i visit hyperlink['href'] return end end raise Rtml::Errors::SimulationError, "Hyperlink not found on screen #{current_screen_id.inspect}: #{path_or_text.inspect}" end def press(which_button) process do app.trigger_button_press(which_button) process # FIXME: is this even necessary? end end # carrier can be any of :visa, :mastercard, :jcb, :amex, :discover, and :debit. # if it's anything else, the 'card.pan' and 'card.scheme' options must be specified. def swipe_card(carrier, options = {}) options.reverse_merge!('card.cardholder_name' => 'Cardholder', 'card.effective_date' => Time.now, 'card.expiry_date' => 1.year.from_now, 'card.input_type' => 1, 'card.issue_number' => 0, 'card.issuer_name' => 'NA', 'card.scheme' => carrier.to_s.upcase, 'card.pan' => case carrier when :visa then '4111111111111111' when :mastercard then '5454545454545454' when :amex then '3434343434343434' when :discover then '6011000012345678' when :jcb then '3083000012345678' when :debit then '1000000012345678' else nil end ) unless options['card.pan'] && options['card.scheme'] raise Rtml::Errors::SimulationError, "Carrier not recognized and/or options are invalid" end card_parsers = on_current_screen("card") card_parsers.each do |card| if card['parser'] == 'mag' && card['parser_params'] == 'read_data' app.update_variables(options) process(:forward) return end end raise Rtml::Errors::SimulationError, "Could not find a suitable card parser (with parser 'mag' and parser_params 'read_data') on screen #{current_screen_id.inspect}" end def fill_in(variable_name, value) variable_name = variable_name.to_s unless variable_name.kind_of?(String) on_current_screen("input").each do |field| if field['name'] == variable_name variables[variable_name] = value return end end raise Rtml::Errors::SimulationError, "Form element for variable #{variable_name.inspect} does not appear on screen #{current_screen_id.inspect}" end def variables app.variable_scope end private delegate :get, :post, :put, :delete, :to => :session def default_options { :process => true } end def on_current_screen(field_name) current_screen ? ((current_screen / field_name) || []) : raise(Rtml::Errors::SimulationError, "Current screen was expected to not be nil") end def build_app(tml) @app = Rtml::Test::TmlApplication.new(tml) end def get_and_build(path) # set up state mocking unless path =~ /ignore_state/ if path =~ /\?/ path.concat "&ignore_state=true" else path.concat "?ignore_state=true" end end get @path = path, {}, @headers if response # actually the only time there's no response should be during tests which stub #get format = response.template.template_format unless format == :rtml raise Rtml::Errors::InvalidFormat, "Response must be in :rtml format; found #{format.inspect}:\n#{response.body}" end build_app(response.body) end end def path_and_screen_from(path) path, screen = path.split(/#/) # query string if screen =~ /\?/ screen, query = screen.split(/\?/) path = "#{path}?#{query}" end [path, screen] end include Rtml::Widgets # we're almost done. App is likely in a "waiting" state; we need to see if it's "waiting" # for something that the simulator can, er, simulate, such as card risk management or # EMV processing. If it is, then we need to fill out whatever variables are expected, and # then send another call to #process. def do_post_processing! return unless current_screen self.widget_entry_points.each do |entry_point| send(entry_point) end end end