diff --git a/lib/rhc/auth/token.rb b/lib/rhc/auth/token.rb index 3de91f9d9..ae9354e06 100644 --- a/lib/rhc/auth/token.rb +++ b/lib/rhc/auth/token.rb @@ -16,8 +16,10 @@ def initialize(opt, auth=nil, store=nil) def to_request(request) if token + debug "Using token authentication" (request[:headers] ||= {})['authorization'] = "Bearer #{token}" elsif auth and (!@allows_tokens or @can_get_token == false) + debug "Bypassing token auth" auth.to_request(request) end request @@ -56,6 +58,7 @@ def token_rejected(response, client) if has_token raise RHC::Rest::TokenExpiredOrInvalid, "Your authorization token is expired or invalid." end + debug "Cannot authenticate via token or password, exiting" return false end @@ -78,6 +81,7 @@ def token_rejected(response, client) return auth.retry_auth?(response, client) unless @can_get_token + debug "Creating a new authorization token" if auth_token = client.new_session(:auth => auth) @fetch_once = true save(auth_token.token) diff --git a/lib/rhc/command_runner.rb b/lib/rhc/command_runner.rb index e09480549..2a3fffc95 100644 --- a/lib/rhc/command_runner.rb +++ b/lib/rhc/command_runner.rb @@ -101,7 +101,7 @@ def run! end def provided_arguments - @args[0, @args.find_index { |arg| arg.start_with?('-') } || @args.length] + @args[0, @args.find_index { |arg| arg != '--' and arg.start_with?('-') } || @args.length] end def global_option(*args, &block) diff --git a/lib/rhc/commands.rb b/lib/rhc/commands.rb index 8ccc47e2c..a050be01f 100644 --- a/lib/rhc/commands.rb +++ b/lib/rhc/commands.rb @@ -34,7 +34,11 @@ def parse_options_and_call_procs *args opts end - remaining = opts.parse! args + # Separate option lists with '--' + remaining = args.split('--').map{ |a| opts.parse!(a) }.inject([]) do |arr, sub| + arr << '--' unless arr.empty? + arr.concat(sub) + end _, config_path = proxy_options.find{ |arg| arg[0] == :config } clean, _ = proxy_options.find{ |arg| arg[0] == :clean } @@ -266,32 +270,42 @@ def self.fill_arguments(cmd, options, args_metadata, options_metadata, args) end raise ArgumentError.new("Missing required option '#{arg}'.") if option_meta[:required] && options[arg].nil? end - # process args - arg_slots = [].fill(nil, 0, args_metadata.length) - fill_args = args.reverse - args_metadata.each_with_index do |arg_meta, i| - # check options - option = arg_meta[:option_symbol] - context_helper = arg_meta[:context_helper] + + available = args.dup + slots = Array.new(args_metadata.length) + args_metadata.each_with_index do |arg, i| + option = arg[:option_symbol] + context_helper = arg[:context_helper] value = options.__hash__[option] if option - value = fill_args.pop if value.nil? value = cmd.send(context_helper) if value.nil? and context_helper - if arg_meta[:arg_type] == :list - fill_args.push(value) unless value.nil? - value = fill_args.reverse - fill_args = [] - elsif value.nil? - raise ArgumentError.new("Missing required argument '#{arg_meta[:name]}'.") if fill_args.empty? + if value.nil? + value = + if arg[:arg_type] == :list + all = [] + while available.first && available.first != '--' + all << available.shift + end + available.shift if available.first == '--' + all + else + available.shift + end + end + + if value.nil? + raise ArgumentError, "Missing required argument '#{arg[:name]}'." unless arg[:optional] + break if available.empty? + else + slots[i] = value + options.__hash__[option] = value if option end - arg_slots[i] = value - options.__hash__[option] = value if option end - raise ArgumentError.new("Too many arguments passed in: #{fill_args.reverse.join(" ")}") unless fill_args.empty? + raise ArgumentError, "Too many arguments passed in: #{available.reverse.join(" ")}" unless available.empty? - arg_slots + slots end def self.commands diff --git a/lib/rhc/commands/app.rb b/lib/rhc/commands/app.rb index 68098feaa..e035e60e5 100644 --- a/lib/rhc/commands/app.rb +++ b/lib/rhc/commands/app.rb @@ -9,6 +9,7 @@ class App < Base description "Creates and controls an OpenShift application. To see the list of all applications use the rhc domain show command. Note that delete is not reversible and will stop your application and then remove the application and repo from the remote server. No local changes are made." syntax "" default_action :help + suppress_wizard summary "Create an application" description <<-DESC @@ -38,19 +39,24 @@ class App < Base DESC syntax " [-n namespace]" - option ["-n", "--namespace NAME"], "Namespace for the application", :context => :namespace_context - option ["-g", "--gear-size size"], "Gear size controls how much memory and CPU your cartridges can use." + option ["-n", "--namespace NAME"], "Namespace for the application" + option ["-g", "--gear-size SIZE"], "Gear size controls how much memory and CPU your cartridges can use." option ["-s", "--scaling"], "Enable scaling for the web cartridge." - option ["-r", "--repo dir"], "Path to the Git repository (defaults to ./$app_name)" + option ["-r", "--repo DIR"], "Path to the Git repository (defaults to ./$app_name)" option ["--from-code URL"], "URL to a Git repository that will become the initial contents of the application" option ["--[no-]git"], "Skip creating the local Git repository." option ["--nogit"], "DEPRECATED: Skip creating the local Git repository.", :deprecated => {:key => :git, :value => false} option ["--[no-]dns"], "Skip waiting for the application DNS name to resolve. Must be used in combination with --no-git" - option ["--enable-jenkins [server_name]"], "Enable Jenkins builds for this application (will create a Jenkins application if not already available). The default name will be 'jenkins' if not specified." - argument :name, "Name for your application", ["-a", "--app name"] - argument :cartridges, "The web framework this application should use", ["-t", "--type cartridge"], :arg_type => :list + option ['--no-keys'], "Skip checking SSH keys during app creation", :hide => true + option ["--enable-jenkins [NAME]"], "Enable Jenkins builds for this application (will create a Jenkins application if not already available). The default name will be 'jenkins' if not specified." + argument :name, "Name for your application", ["-a", "--app NAME"], :optional => true + argument :cartridges, "The web framework this application should use", ["-t", "--type CARTRIDGE"], :optional => true, :arg_type => :list #argument :additional_cartridges, "A list of other cartridges such as databases you wish to add. Cartridges can also be added later using 'rhc cartridge add'", [], :arg_type => :list def create(name, cartridges) + check_config! + + check_name!(name) + cartridges = check_cartridges(cartridges, &require_one_web_cart) options.default \ @@ -59,8 +65,7 @@ def create(name, cartridges) raise ArgumentError, "You have named both your main application and your Jenkins application '#{name}'. In order to continue you'll need to specify a different name with --enable-jenkins or choose a different application name." if jenkins_app_name == name && enable_jenkins? - raise RHC::Rest::DomainNotFoundException.new("No domains found. Please create a domain with 'rhc domain create ' before creating applications.") if rest_client.domains.empty? - rest_domain = rest_client.find_domain(options.namespace) + rest_domain = check_domain! rest_app = nil cart_names = cartridges.collect do |c| @@ -121,6 +126,9 @@ def create(name, cartridges) end if build_app_exists end + debug "Checking SSH keys through the wizard" + check_sshkeys! unless options.no_keys + if options.dns paragraph do say "Waiting for your DNS name to be available ... " @@ -139,9 +147,6 @@ def create(name, cartridges) if options.git paragraph do - debug "Checking SSH keys through the wizard" - check_sshkeys! unless options.noprompt - say "Downloading the application Git repository ..." paragraph do begin @@ -335,9 +340,40 @@ def require_one_web_cart end def check_sshkeys! + return unless interactive? RHC::SSHWizard.new(rest_client, config, options).run end + def check_name!(name) + return unless name.blank? + + paragraph{ say "When creating an application, you must provide a name and a cartridge from the list below:" } + paragraph{ list_cartridges(standalone_cartridges) } + + raise ArgumentError, "Please specify the name of the application and the web cartridge to install" + end + + def check_config! + return if not interactive? or (!options.clean && config.has_local_config?) or (options.server && (options.rhlogin || options.token)) + RHC::EmbeddedWizard.new(config, options).run + end + + def check_domain! + if options.namespace + rest_client.find_domain(options.namespace) + else + if rest_client.domains.empty? + raise RHC::Rest::DomainNotFoundException, "No domains found. Please create a domain with 'rhc domain create ' before creating applications." unless interactive? + RHC::DomainWizard.new(config, options, rest_client).run + end + domain = rest_client.domains.first + raise RHC::Rest::DomainNotFoundException, "No domains found. Please create a domain with 'rhc domain create ' before creating applications." unless domain + options.namespace = domain.id + domain + end + end + + def gear_groups_for_app(app_name) rest_client.find_application_gear_groups(options.namespace, app_name) end diff --git a/lib/rhc/commands/base.rb b/lib/rhc/commands/base.rb index 7e2debc9e..668c95a26 100644 --- a/lib/rhc/commands/base.rb +++ b/lib/rhc/commands/base.rb @@ -32,6 +32,7 @@ def rest_client(opts={}) @rest_client ||= begin auth = RHC::Auth::Basic.new(options) auth = RHC::Auth::Token.new(options, auth, token_store) if (options.use_authorization_tokens || options.token) && !(options.rhlogin && options.password) + debug "Authenticating with #{auth.class}" client_from_options(:auth => auth) end end @@ -129,10 +130,8 @@ def self.option(switches, description, options={}) } end - def self.argument(name, description, switches, options={}) + def self.argument(name, description, switches=[], options={}) arg_type = options[:arg_type] - raise ArgumentError("Only the last argument descriptor for an action can be a list") if arg_type == :list and list_argument_defined? - list_argument_defined true if arg_type == :list option_symbol = Commander::Runner.switch_to_sym(switches.last) args_metadata << {:name => name, @@ -140,6 +139,7 @@ def self.argument(name, description, switches, options={}) :switches => switches, :context_helper => options[:context], :option_symbol => option_symbol, + :optional => options[:optional], :arg_type => arg_type} end @@ -149,12 +149,6 @@ def self.default_action(action) end private - def self.list_argument_defined(bool) - options[:list_argument_defined] = bool - end - def self.list_argument_defined? - options[:list_argument_defined] - end def self.options_metadata options[:options] ||= [] end diff --git a/lib/rhc/commands/domain.rb b/lib/rhc/commands/domain.rb index 382cd8e97..20b1ffd03 100644 --- a/lib/rhc/commands/domain.rb +++ b/lib/rhc/commands/domain.rb @@ -89,7 +89,7 @@ def status def delete(namespace) domain = rest_client.find_domain namespace - say "Deleting domain '#{namespace}'" + say "Deleting domain '#{namespace}' ... " begin domain.destroy @@ -97,7 +97,8 @@ def delete(namespace) raise RHC::Exception.new("Domain contains applications. Delete applications first.", 128) end - results { say "Success!" } + success "deleted" + 0 end end diff --git a/lib/rhc/commands/sshkey.rb b/lib/rhc/commands/sshkey.rb index 4f0d06b9f..61f85bd0e 100644 --- a/lib/rhc/commands/sshkey.rb +++ b/lib/rhc/commands/sshkey.rb @@ -69,10 +69,12 @@ def add(name, key) summary 'Remove SSH key from your account' syntax '' alias_action :delete - argument :name, 'SSH key to remove', [] + argument :name, 'Name of SSH key to remove' def remove(name) + say "Removing the key '#{name} ... " rest_client.delete_key(name) - results { say "SSH key '#{name}' has been removed" } + + success "removed" 0 end diff --git a/lib/rhc/core_ext.rb b/lib/rhc/core_ext.rb index 6ec70d9fb..0c93c2d2b 100644 --- a/lib/rhc/core_ext.rb +++ b/lib/rhc/core_ext.rb @@ -23,6 +23,23 @@ def to_json(options=nil) end end +class Array + # From rails + def split(value = nil) + using_block = block_given? + + inject([[]]) do |results, element| + if (using_block && yield(element)) || (value == element) + results << [] + else + results.last << element + end + + results + end + end +end + class File def chunk(chunk_size=1024) yield read(chunk_size) until eof? diff --git a/lib/rhc/helpers.rb b/lib/rhc/helpers.rb index 0db8be576..4877380d0 100644 --- a/lib/rhc/helpers.rb +++ b/lib/rhc/helpers.rb @@ -180,6 +180,10 @@ def certificate_file(file) # Output helpers # + def interactive? + $stdout.tty? and not options.noprompt + end + def debug(msg) $stderr.puts "DEBUG: #{msg}" if debug? end diff --git a/lib/rhc/rest/mock.rb b/lib/rhc/rest/mock.rb index f979c1fbb..a801959b4 100644 --- a/lib/rhc/rest/mock.rb +++ b/lib/rhc/rest/mock.rb @@ -120,7 +120,7 @@ def stub_add_authorization(params) to_return(new_authorization(params)) end def stub_no_keys - stub_api_request(:get, 'broker/rest/user/keys', mock_user_auth).to_return(no_keys) + stub_api_request(:get, 'broker/rest/user/keys', mock_user_auth).to_return(empty_keys) end def stub_mock_ssh_keys(name='test') stub_api_request(:get, 'broker/rest/user/keys', mock_user_auth). @@ -159,7 +159,7 @@ def stub_one_key(name) }) end def stub_no_domains - stub_api_request(:get, 'broker/rest/domains', mock_user_auth).to_return(no_domains) + stub_api_request(:get, 'broker/rest/domains', mock_user_auth).to_return(empty_domains) end def stub_one_domain(name) stub_api_request(:get, 'broker/rest/domains', mock_user_auth). @@ -221,10 +221,10 @@ def test_and_raise EOM end - def no_keys + def empty_keys empty_response_list('keys') end - def no_domains + def empty_domains empty_response_list('domains') end diff --git a/lib/rhc/wizard.rb b/lib/rhc/wizard.rb index 965a70426..4cae78e73 100644 --- a/lib/rhc/wizard.rb +++ b/lib/rhc/wizard.rb @@ -12,16 +12,26 @@ def self.has_configuration? DEFAULT_MAX_LENGTH = 16 - STAGES = [:greeting_stage, - :login_stage, - :create_config_stage, - :config_ssh_key_stage, - :upload_ssh_key_stage, - :install_client_tools_stage, - :setup_test_stage, - :config_namespace_stage, - :show_app_info_stage, - :finalize_stage] + CONFIG_STAGES = [ + :login_stage, + :create_config_stage, + ] + KEY_STAGES = [ + :config_ssh_key_stage, + :upload_ssh_key_stage, + ] + TEST_STAGES = [ + :install_client_tools_stage, + :setup_test_stage, + ] + NAMESPACE_STAGES = [ + :config_namespace_stage, + ] + APP_STAGES = [ + :show_app_info_stage, + ] + STAGES = [:greeting_stage] + CONFIG_STAGES + KEY_STAGES + TEST_STAGES + NAMESPACE_STAGES + APP_STAGES + [:finalize_stage] + def stages STAGES end @@ -57,55 +67,64 @@ def run end protected - include RHC::Helpers - include RHC::SSHHelpers - include RHC::GitHelpers - include RHC::CartridgeHelpers - attr_reader :config, :options - attr_accessor :auth, :user - - def openshift_server - options.server || config['libra_server'] || "openshift.redhat.com" - end - def new_client_for_options - client_from_options({ - :auth => auth, - }) - end + include RHC::Helpers + include RHC::SSHHelpers + include RHC::GitHelpers + include RHC::CartridgeHelpers + attr_reader :config, :options + attr_accessor :auth, :user + attr_writer :rest_client - def core_auth - @core_auth ||= RHC::Auth::Basic.new(options) - end + def debug? + @debug + end - def token_auth - RHC::Auth::Token.new(options, core_auth, token_store) - end + def hostname + Socket.gethostname + end - def auth(reset=false) - @auth = nil if reset - @auth ||= begin - if options.token - token_auth - else - core_auth - end + def openshift_server + options.server || config['libra_server'] || "openshift.redhat.com" + end + + def new_client_for_options + client_from_options({ + :auth => auth, + }) + end + + def core_auth + @core_auth ||= RHC::Auth::Basic.new(options) + end + + def token_auth + RHC::Auth::Token.new(options, core_auth, token_store) + end + + def auth(reset=false) + @auth = nil if reset + @auth ||= begin + if options.token + token_auth + else + core_auth end - end + end + end - def token_store - @token_store ||= RHC::Auth::TokenStore.new(config.home_conf_path) - end + def token_store + @token_store ||= RHC::Auth::TokenStore.new(config.home_conf_path) + end - def username - options.rhlogin || (auth.username if auth.respond_to?(:username)) - end + def username + options.rhlogin || (auth.username if auth.respond_to?(:username)) + end - def print_dot - $terminal.instance_variable_get(:@output).print('.') - end + def print_dot + $terminal.instance_variable_get(:@output).print('.') + end - private # cache SSH keys from the REST client def ssh_keys @@ -117,6 +136,29 @@ def clear_ssh_keys_cache @ssh_keys = nil end + # return true if the account has the public key defined by + # RHC::Config::ssh_pub_key_file_path + def ssh_key_uploaded? + ssh_keys.present? && ssh_keys.any? { |k| k.fingerprint.present? && k.fingerprint == fingerprint_for_default_key } + end + + def existing_keys_info + return unless ssh_keys + indent{ ssh_keys.each{ |key| paragraph{ display_key(key) } } } + end + + def applications + @applications ||= rest_client.domains.map(&:applications).flatten + end + + def namespace_optional? + true + end + + # + # Stages + # + def greeting_stage info "OpenShift Client Tools (RHC) Setup Wizard" @@ -216,17 +258,6 @@ def config_ssh_key_stage true end - # return true if the account has the public key defined by - # RHC::Config::ssh_pub_key_file_path - def ssh_key_uploaded? - ssh_keys.present? && ssh_keys.any? { |k| k.fingerprint.present? && k.fingerprint == fingerprint_for_default_key } - end - - def existing_keys_info - return unless ssh_keys - indent{ ssh_keys.each{ |key| paragraph{ display_key(key) } } } - end - def upload_ssh_key_stage return true if ssh_key_uploaded? @@ -436,12 +467,7 @@ def setup_test_stage end def all_test_methods - private_methods.select {|m| m.to_s.start_with? 'test_'} - end - - # cached list of applications needed for test stage - def applications - @applications ||= rest_client.domains.map(&:applications).flatten + (protected_methods + private_methods).select {|m| m.to_s.start_with? 'test_'} end ### @@ -487,13 +513,14 @@ def finalize_stage def config_namespace(namespace) # skip if string is empty - if namespace.nil? or namespace.chomp.length == 0 + if namespace_optional? and (namespace.nil? or namespace.chomp.blank?) paragraph{ info "You may create a namespace later through 'rhc domain create'" } return true end begin domain = rest_client.add_domain(namespace) + options.namespace = namespace success "Your domain name '#{domain.id}' has been successfully created" rescue RHC::Rest::ValidationException => e @@ -509,9 +536,7 @@ def ask_for_namespace namespace = nil paragraph do begin - namespace = ask "Please enter a namespace (letters and numbers only) ||: " do |q| - #q.validate = lambda{ |p| RHC::check_namespace p } - #q.responses[:not_valid] = 'The namespace value must contain only letters and/or numbers (A-Za-z0-9):' + namespace = ask "Please enter a namespace (letters and numbers only)#{namespace_optional? ? " ||" : ""}: " do |q| q.responses[:ask_on_error] = '' end end while !config_namespace(namespace) @@ -553,21 +578,9 @@ def windows_install EOF end - - def debug? - @debug - end - - def hostname - Socket.gethostname - end - - protected - attr_writer :rest_client end class RerunWizard < Wizard - def finalize_stage section :top => 1 do success "Your client tools are now configured." @@ -576,11 +589,36 @@ def finalize_stage end end + class EmbeddedWizard < Wizard + def stages + super - APP_STAGES - KEY_STAGES - [:setup_test_stage] + end + + def finalize_stage + true + end + end + + class DomainWizard < Wizard + def initialize(*args) + client = args.length == 3 ? args.pop : nil + super *args + self.rest_client = client || new_client_for_options + end + + def stages + [:config_namespace_stage] + end + + protected + def namespace_optional? + false + end + end + class SSHWizard < Wizard - STAGES = [:config_ssh_key_stage, - :upload_ssh_key_stage] def stages - STAGES + KEY_STAGES end def initialize(rest_client, config, options) diff --git a/spec/rhc/command_spec.rb b/spec/rhc/command_spec.rb index c224f2934..171a7b1a8 100644 --- a/spec/rhc/command_spec.rb +++ b/spec/rhc/command_spec.rb @@ -122,6 +122,12 @@ def execute(testarg); 1; end summary "Test command execute-list" def execute_list(args); 1; end + argument :arg1, "Test arg", ['--test'], :optional => true + argument :arg2, "Test arg list", ['--test2'], :arg_type => :list, :optional => true + argument :arg3, "Test arg list", ['--test3'], :arg_type => :list, :optional => true + summary "Test command execute-vararg" + def execute_vararg(arg1, arg2, arg3); 1; end + RHC::Helpers.global_option '--test-context', 'Test', :context => :context_var def execute_implicit end @@ -142,7 +148,7 @@ def context_var Static end - it("should register itself") { expect { subject }.to change(commands, :length).by(6) } + it("should register itself") { expect { subject }.to change(commands, :length).by(7) } it("should have an object name of the class") { subject.object_name.should == 'static' } context 'and when test is called' do @@ -178,6 +184,14 @@ def context_var it('should make the option available') { command_for('static-execute-list', '1', '2', '3').send(:options).tests.should == ['1','2','3'] } end + context 'and when execute_vararg is called' do + it{ expects_running('static-execute-vararg').should call(:execute_vararg).on(instance).with(nil, nil, nil) } + it{ expects_running('static-execute-vararg', '1', '2', '3').should call(:execute_vararg).on(instance).with('1', ['2', '3'], []) } + it("handles a list separator"){ expects_running('static-execute-vararg', '1', '2', '--', '3').should call(:execute_vararg).on(instance).with('1', ['2'], ['3']) } + it{ command_for('static-execute-vararg', '1', '2', '--', '3').send(:options).test.should == '1' } + it{ command_for('static-execute-vararg', '1', '2', '--', '3').send(:options).test2.should == ['2'] } + it{ command_for('static-execute-vararg', '1', '2', '--', '3').send(:options).test3.should == ['3'] } + end context 'and when execute is called with a contextual global option' do it("calls the helper") { command_for('static', 'execute-implicit').send(:options).test_context.should == 'contextual' } end diff --git a/spec/rhc/commands/app_spec.rb b/spec/rhc/commands/app_spec.rb index 63571970b..5522128ec 100644 --- a/spec/rhc/commands/app_spec.rb +++ b/spec/rhc/commands/app_spec.rb @@ -6,10 +6,11 @@ describe RHC::Commands::App do let!(:rest_client){ MockRestClient.new } - before(:each) do + let!(:config){ user_config } + before{ RHC::Config.stub(:home_dir).and_return('/home/mock_user') } + before do FakeFS.activate! FakeFS::FileSystem.clear - user_config RHC::Helpers.send(:remove_const, :MAX_RETRIES) rescue nil RHC::Helpers.const_set(:MAX_RETRIES, 3) @instance = RHC::Commands::App.new @@ -38,7 +39,7 @@ end context 'app' do - let(:arguments) { ['app', '--noprompt', '--config', 'test.conf', '-l', 'test@test.foo', '-p', 'password'] } + let(:arguments) { ['app'] } it { run_output.should match('Usage:') } end end @@ -49,11 +50,44 @@ it("shows number of started"){ subject.send(:gear_group_state, ['started', 'idle']).should == '1/2 started' } end - describe 'app create' do - before(:each) do - domain = rest_client.add_domain("mockdomain") + describe '#check_domain!' do + let(:rest_client){ stub('RestClient') } + let(:domain){ stub('Domain', :id => 'test') } + before{ subject.stub(:rest_client).and_return(rest_client) } + let(:interactive){ false } + before{ subject.stub(:interactive?).and_return(interactive) } + + context "when no options are provided and there is one domain" do + before{ rest_client.should_receive(:domains).twice.and_return([domain]) } + it("should load the first domain"){ subject.send(:check_domain!).should == domain } + after{ subject.send(:options).namespace.should == domain.id } + end + + context "when no options are provided and there are no domains" do + before{ rest_client.should_receive(:domains).and_return([]) } + it("should load the first domain"){ expect{ subject.send(:check_domain!) }.to raise_error(RHC::Rest::DomainNotFoundException) } + after{ subject.send(:options).namespace.should be_nil } + end + + context "when valid namespace is provided" do + before{ subject.send(:options)[:namespace] = 'test' } + before{ rest_client.should_receive(:find_domain).with('test').and_return(domain) } + it("should load the requested domain"){ subject.send(:check_domain!).should == domain } + after{ subject.send(:options).namespace.should == 'test' } end + context "when interactive and no domains" do + let(:interactive){ true } + before{ rest_client.should_receive(:domains).twice.and_return([]) } + before{ RHC::DomainWizard.should_receive(:new).and_return(stub(:run => true)) } + it("should raise if the wizard doesn't set the option"){ expect{ subject.send(:check_domain!) }.to raise_error(RHC::Rest::DomainNotFoundException) } + after{ subject.send(:options).namespace.should be_nil } + end + end + + describe 'app create' do + before{ rest_client.add_domain("mockdomain") } + context "when we ask for help with the alias" do before{ FakeFS.deactivate! } context do @@ -66,6 +100,68 @@ end end + context "when run with no arguments" do + before{ FakeFS.deactivate! } + let(:arguments){ ['create-app'] } + it{ run_output.should match "Usage: rhc app-create " } + it{ run_output.should match "When creating an application, you must provide a name and a cartridge from the list below:" } + it{ run_output.should match "mock_standalone_cart-1" } + it{ run_output.should match "Please specify the name of the application" } + end + + context "when dealing with config" do + subject{ described_class.new(Commander::Command::Options.new(options)) } + let(:wizard){ s = stub('Wizard'); RHC::EmbeddedWizard.should_receive(:new).and_return(s); s } + let(:options){ nil } + let(:interactive){ true } + before{ subject.should_receive(:interactive?).at_least(1).times.and_return(interactive) } + before{ subject.stub(:check_sshkeys!) } + + it("should run the wizard"){ expect{ subject.create('name', ['mock_standalone_cart-1']) }.to call(:run).on(wizard).and_stop } + + context "when has config" do + let(:options){ {:server => 'test', :rhlogin => 'foo'} } + before{ subject.send(:config).should_receive(:has_local_config?).and_return(true) } + it("should not run the wizard"){ expect{ subject.create('name', ['mock_standalone_cart-1']) }.to not_call(:new).on(RHC::EmbeddedWizard) } + end + + context "when has no config" do + before{ subject.send(:config).should_receive(:has_local_config?).and_return(false) } + it("should run the wizard"){ expect{ subject.create('name', ['mock_standalone_cart-1']) }.to call(:new).on(RHC::EmbeddedWizard).and_stop } + end + + context "when not interactive" do + let(:interactive){ false } + it("should not run the wizard"){ expect{ subject.create('name', ['mock_standalone_cart-1']) }.to not_call(:new).on(RHC::EmbeddedWizard) } + end + end + + context "when dealing with ssh keys" do + subject{ described_class.new(options) } + let(:wizard){ s = stub('Wizard'); RHC::SSHWizard.should_receive(:new).and_return(s); s } + let(:options){ Commander::Command::Options.new(:server => 'foo.com', :rhlogin => 'test') } + let(:interactive){ true } + before{ subject.should_receive(:interactive?).at_least(1).times.and_return(interactive) } + before{ subject.should_receive(:check_config!) } + + it("should run the wizard"){ expect{ subject.create('name', ['mock_standalone_cart-1']) }.to call(:run).on(wizard).and_stop } + + context "when not interactive" do + let(:interactive){ false } + it("should not run the wizard"){ expect{ subject.create('name', ['mock_standalone_cart-1']) }.to not_call(:new).on(RHC::SSHWizard) } + end + end + + context "when in full interactive mode with no keys, domain, or config" do + let!(:config){ base_config } + before{ RHC::Config.any_instance.stub(:has_local_config?).and_return(false) } + before{ described_class.any_instance.stub(:interactive?).and_return(true) } + before{ rest_client.domains.clear } + let(:arguments) { ['app', 'create', 'app1', 'mock_standalone_cart-1'] } + # skips login stage and insecure check because of mock rest client, doesn't check keys + it { run_output(['mydomain', 'y', 'mykey']).should match(/This wizard.*Checking your namespace.*Your domain name 'mydomain' has been successfully created.*Creating application.*Your public SSH key.*Uploading key 'mykey' .*Downloading the application.*Success/m) } + end + context 'when run without a cart' do before{ FakeFS.deactivate! } let(:arguments) { ['app', 'create', 'app1', '--noprompt', '--timeout', '10', '--config', 'test.conf', '-l', 'test@test.foo', '-p', 'password'] } @@ -273,7 +369,7 @@ before(:each) do @domain = rest_client.add_domain("mockdomain") @instance.stub(:git_clone_application) { raise RHC::GitException } - @instance.should_not_receive(:check_sshkeys!) + @instance.stub(:check_sshkeys!) end context 'when run with error in git clone' do diff --git a/spec/rhc/wizard_spec.rb b/spec/rhc/wizard_spec.rb index e5efc7e3d..6bf7284ea 100644 --- a/spec/rhc/wizard_spec.rb +++ b/spec/rhc/wizard_spec.rb @@ -720,3 +720,12 @@ def initialize end end end + +describe RHC::DomainWizard do + context "with a rest client" do + let(:rest_client){ stub } + it{ described_class.new(nil, nil, rest_client).rest_client.should == rest_client } + it{ subject.stages == [:config_namespace_stage] } + it{ expect{ described_class.new(nil, nil, rest_client).send(:config_namespace, '') }.to call(:add_domain).on(rest_client).and_stop } + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b0d3935b9..11c23ad5f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -365,20 +365,28 @@ module ExitCodeMatchers end module CommanderInvocationMatchers + InvocationMatch = Class.new(RuntimeError) + RSpec::Matchers.define :call do |method| chain :on do |object| @object = object end - chain :with do |args| + chain :with do |*args| @args = args end + chain :and_stop do + @stop = true + end match do |block| e = @object.should_receive(method) - e.with(@args) if @args + e.and_raise(InvocationMatch) if @stop + e.with(*@args) if @args begin block.call true + rescue InvocationMatch => e + true rescue SystemExit => e false end @@ -387,6 +395,29 @@ module CommanderInvocationMatchers "expect block to invoke '#{method}' on #{@object} with #{@args}" end end + + RSpec::Matchers.define :not_call do |method| + chain :on do |object| + @object = object + end + chain :with do |*args| + @args = args + end + + match do |block| + e = @object.should_not_receive(method) + e.with(*@args) if @args + begin + block.call + true + rescue SystemExit => e + false + end + end + description do + "expect block to invoke '#{method}' on #{@object} with #{@args}" + end + end end module ColorMatchers diff --git a/spec/wizard_spec_helper.rb b/spec/wizard_spec_helper.rb index 4b688282b..cba2d40a8 100644 --- a/spec/wizard_spec_helper.rb +++ b/spec/wizard_spec_helper.rb @@ -130,7 +130,7 @@ def should_check_remote_server next_stage.should_not be_nil last_output do |s| - s.should match(/Checking common problems \.+.+?done/) + s.should match(/Checking common problems \.+.+? done/) end end @@ -161,6 +161,7 @@ def should_create_a_namespace s.should match(/Checking your namespace .*none/) s.should match(/(?:Too long.*?){2}/m) end + subject.send(:options).__hash__[:namespace].should == 'testnamespace' end def should_skip_creating_namespace @@ -173,12 +174,14 @@ def should_skip_creating_namespace s.should match("You will not be able to create applications without first creating a namespace") s.should match("You may create a namespace later through 'rhc domain create'") end + subject.send(:options).__hash__[:namespace].should be_nil end def should_find_a_namespace(namespace) next_stage.should_not be_nil last_output.should match(/Checking your namespace .*#{namespace}/) + subject.send(:options).__hash__[:namespace].should be_nil end def should_list_types_of_apps_to_create