require 'thor' require 'pact_broker/client/cli/thor_unknown_options_monkey_patch' require 'pact_broker/client/hash_refinements' module PactBroker module Client module CLI class AuthError < ::Thor::Error; end ## # Custom Thor task allows the following: # # `--option 1 --option 2` to be interpreted as `--option 1 2` (the standard Thor format for multiple value options) # class CustomThor < ::Thor using PactBroker::Client::HashRefinements EM_DASH = "\u2014" check_unknown_options! no_commands do def self.exit_on_failure? true end # Provide a wrapper method that can be stubbed in tests def self.exit_with_error_code exit(1) end def self.start given_args = ARGV, config = {} check_for_mdash!(given_args) super(massage_args(given_args)) end def self.massage_args argv add_broker_config_from_environment_variables(turn_muliple_tag_options_into_array(handle_help(argv))) end def self.add_broker_config_from_environment_variables argv return argv if argv[0] == '--help' || argv[0] == 'help' || argv.empty? add_option_from_environment_variable(argv, 'broker-base-url', 'b', 'PACT_BROKER_BASE_URL') end def self.add_option_from_environment_variable argv, long_name, short_name, environment_variable_name option_options = ["--#{long_name}", "--#{long_name.gsub('-','_')}", "-#{short_name}"] if (argv & option_options).empty? && ENV[environment_variable_name] argv + ["--#{long_name}", ENV[environment_variable_name]] else argv end end # Thor expects help to be invoked by typing `help `, which is very odd. # Add support for `command --help|-h` by massaging the arguments into the format that Thor expects. def self.handle_help(argv) if argv.last == "--help" || argv.last == "-h" argv[0..-3] + ["help", argv[-2]].compact else argv end end def self.check_for_mdash!(argv) if (word_with_mdash = argv.find{ |arg | arg.include?(EM_DASH) }) # Can't use the `raise Thor::Error` approach here (which is designed to show the error without a backtrace) # because the exception is not handled within the Thor code, and will show an ugly backtrace. $stdout.puts "The argument '#{word_with_mdash}' contains an em dash (the long dash you get in Microsoft Word when you type two short dashes in a row). Please replace it with a normal dash and try again." exit_with_error_code end end # other task names, help, and the help shortcuts def self.known_first_arguments @known_first_arguments ||= tasks.keys + ::Thor::HELP_MAPPINGS + ['help'] end def self.turn_muliple_tag_options_into_array argv new_argv = [] opt_name = nil argv.each_with_index do | word, i | if word.start_with?('-') if word.include?('=') opt_name, opt_value = word.split('=', 2) existing = new_argv.find { | a | a.first == opt_name } if existing existing << opt_value else new_argv << [opt_name, opt_value] end else opt_name = word existing = new_argv.find { | a | a.first == opt_name } if !existing new_argv << [word] end end else if opt_name existing = new_argv.find { | a | a.first == opt_name } existing << word opt_name = nil else new_argv << [word] end end end new_argv.flatten end # If you try and generate a uuid, and the PACT_BROKER_... env vars are set, it will cause # generate_uuid to be called with parameters that it doesn't declare, and hence, throw an error. # This is a dirty hack that stops that happening! def self.ignored_and_hidden_potential_options_from_environment_variables method_option :broker_base_url, hide: true method_option :broker_username, hide: true method_option :broker_password, hide: true method_option :broker_token, hide: true end def self.shared_authentication_options method_option :broker_base_url, required: true, aliases: "-b", desc: "The base URL of the Pact Broker" method_option :broker_username, aliases: "-u", desc: "Pact Broker basic auth username" method_option :broker_password, aliases: "-p", desc: "Pact Broker basic auth password" method_option :broker_token, aliases: "-k", desc: "Pact Broker bearer token" method_option :verbose, aliases: "-v", type: :boolean, default: false, required: false, desc: "Verbose output." end def self.verbose_option method_option :verbose, aliases: "-v", type: :boolean, default: false, required: false, desc: "Verbose output." end def self.output_option_json_or_text method_option :output, aliases: "-o", desc: "json or text", default: 'text' end def self.output_option_json_or_table method_option :output, aliases: "-o", desc: "json or table", default: 'table' end def params_from_options(keys) keys.each_with_object({}) { | key, p | p[key] = options[key] } end def pact_broker_client_options client_options = { verbose: options.verbose, pact_broker_base_url: options.broker_base_url&.chomp('/') } client_options[:token] = options.broker_token || ENV['PACT_BROKER_TOKEN'] if options.broker_username || ENV['PACT_BROKER_USERNAME'] client_options[:basic_auth] = { username: options.broker_username || ENV['PACT_BROKER_USERNAME'], password: options.broker_password || ENV['PACT_BROKER_PASSWORD'] }.compact end client_options.compact end def validate_credentials if options.broker_username && options.broker_token raise AuthError, "You cannot provide both a username/password and a bearer token. If your Pact Broker uses a bearer token, please remove the username and password configuration." end end end end end end end