use strict; use warnings; # Force use of English in tests for the moment, for those users that # have a different locale set, since errors are hardcoded below use POSIX qw(setlocale locale_h); setlocale( LC_ALL, "C" ); use FindBin qw($Bin $Script); use lib "$Bin/../lib"; # fix path for finding our fake xterm on headless systems that do # not have it installed, such as TravisCI via github BEGIN { $ENV{PATH} = $ENV{PATH} . ':' . $Bin . '/bin'; } use Test::More; use Test::Trap; use File::Which qw(which); use File::Temp qw(tempdir); use Test::Differences; use Readonly; BEGIN { use_ok("App::ClusterSSH::Config") || BAIL_OUT('failed to use module'); } my $config; $config = App::ClusterSSH::Config->new(); isa_ok( $config, 'App::ClusterSSH::Config' ); Readonly::Hash my %default_config => { terminal => "xterm", terminal_args => "", terminal_title_opt => "-T", terminal_colorize => 1, terminal_bg_style => 'dark', terminal_allow_send_events => "-xrm '*.VT100.allowSendEvents:true'", terminal_font => "6x13", terminal_size => "80x24", use_hotkeys => "yes", key_quit => "Alt-q", key_addhost => "Control-Shift-plus", key_clientname => "Alt-n", key_history => "Alt-h", key_localname => "Alt-l", key_retilehosts => "Alt-r", key_macros_enable => "Alt-p", key_paste => "Control-v", key_username => "Alt-u", key_user_1 => "Alt-1", key_user_2 => "Alt-2", key_user_3 => "Alt-3", key_user_4 => "Alt-4", mouse_paste => "Button-2", auto_quit => "yes", auto_close => 5, window_tiling => "yes", window_tiling_direction => "right", console_position => "", screen_reserve_top => 0, screen_reserve_bottom => 60, screen_reserve_left => 0, screen_reserve_right => 0, terminal_reserve_top => 5, terminal_reserve_bottom => 0, terminal_reserve_left => 5, terminal_reserve_right => 0, terminal_decoration_height => 10, terminal_decoration_width => 8, ssh => '/usr/bin/ssh', console => 'console', console_args => '', rsh => 'rsh', rsh_args => "", telnet => 'telnet', telnet_args => "", ssh => 'ssh', ssh_args => "", sftp => 'sftp', sftp_args => "", extra_tag_file => "", extra_cluster_file => "", external_cluster_command => '', external_command_mode => '0600', external_command_pipe => '', unmap_on_redraw => "no", show_history => 0, history_width => 40, history_height => 10, hostname_override => '', command => q{}, command_pre => q{}, command_post => q{}, title => q{15CONFIG.T}, comms => q{ssh}, hide_menu => 0, max_host_menu_items => 30, macros_enabled => 'yes', macro_servername => '%s', macro_hostname => '%h', macro_username => '%u', macro_newline => '%n', macro_version => '%v', macro_user_1 => '%1', macro_user_2 => '%2', macro_user_3 => '%3', macro_user_4 => '%4', macro_user_1_command => '', macro_user_2_command => '', macro_user_3_command => '', macro_user_4_command => '', max_addhost_menu_cluster_items => 6, menu_send_autotearoff => 0, menu_host_autotearoff => 0, unique_servers => 0, use_all_a_records => 0, use_natural_sort => 0, send_menu_xml_file => $ENV{HOME} . '/.clusterssh/send_menu', # Debian #842965 auto_wm_decoration_offsets => "no", # other bits inheritted from App::ClusterSSH::Base lang => 'en', user => '', rows => -1, cols => -1, fillscreen => 'no', }; my %expected = %default_config; is_deeply( $config, \%expected, 'default config is correct' ); $config = App::ClusterSSH::Config->new(); trap { $config = $config->validate_args( whoops => 'not there', doesnt_exist => 'whoops', ); }; isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' ); is( $trap->die, 'Unknown configuration parameters: doesnt_exist,whoops' . $/, 'got correct error message' ); is_deeply( $trap->die->unknown_config, [ 'doesnt_exist', 'whoops' ], 'Picked up unknown config array' ); isa_ok( $config, "App::ClusterSSH::Config" ); $expected{extra_cluster_file} = '/etc/filename'; $expected{rsh_args} = 'some args'; $expected{max_addhost_menu_cluster_items} = 120; trap { $config = $config->validate_args( extra_cluster_file => '/etc/filename', rsh_args => 'some args', max_addhost_menu_cluster_items => 120, ); }; is( $trap->die, undef, 'validated ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); is_deeply( $config, \%expected, 'default config is correct' ); $config = App::ClusterSSH::Config->new(); %expected = %default_config; my $file = "$Bin/$Script.doesntexist"; trap { $config = $config->parse_config_file( $file, ); }; isa_ok( $trap->die, 'App::ClusterSSH::Exception::LoadFile' ); is( $trap->die, "Unable to read file $file: No such file or directory" . $/, 'got correct error message' ); $file = "$Bin/$Script.file1"; note("using $file"); $config = App::ClusterSSH::Config->new(); %expected = %default_config; $expected{screen_reserve_left} = 100; $expected{screen_reserve_right} = 100; $expected{screen_reserve_top} = 100; $expected{screen_reserve_bottom} = 160; # Note: the parse_config here removes the key_user_x entries delete( $expected{"key_user_$_"} ) for (qw/ 1 2 3 4 /); trap { $config = $config->parse_config_file( $file, ); }; is( $trap->leaveby, 'return', 'returned ok' ); is( $trap->die, undef, 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is_deeply( $config, \%expected, 'amended config is correct' ); $file = "$Bin/$Script.file2"; note("using $file"); $config = App::ClusterSSH::Config->new(); %expected = %default_config; trap { $config = $config->parse_config_file( $file, ); }; is( $trap->leaveby, 'die', 'died ok' ); isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' ); is( $trap->die, 'Unknown configuration parameters: missing,rubbish' . $/, 'die message correct' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is_deeply( $config, \%expected, 'amended config is correct' ); $file = "$Bin/$Script.file3"; note("using $file"); $config = App::ClusterSSH::Config->new(); %expected = %default_config; trap { $config = $config->parse_config_file( $file, ); }; is( $trap->leaveby, 'return', 'returned ok' ); is( $trap->die, undef, 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); note('find_binary tests'); my $path; $config = App::ClusterSSH::Config->new(); trap { $path = $config->find_binary(); }; is( $trap->leaveby, 'die', 'died ok' ); isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, 'argument not provided' . $/, 'die message correct' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is_deeply( $config, \%expected, 'amended config is correct' ); trap { $path = $config->find_binary('missing'); }; is( $trap->leaveby, 'die', 'died ok' ); isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, '"missing" binary not found - please amend $PATH or the cssh config file' . $/, 'die message correct' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is_deeply( $config, \%expected, 'amended config is correct' ); trap { $path = $config->find_binary('ls'); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is_deeply( $config, \%expected, 'amended config is correct' ); is( $path, 'ls', 'Found correct path to "ls"' ); # check for a binary already found my $newpath; trap { $newpath = $config->find_binary($path); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is_deeply( $config, \%expected, 'amended config is correct' ); is( $path, 'ls', 'Found correct path to "ls"' ); is( $path, $newpath, 'No change made from find_binary' ); # give false path to force another search trap { $newpath = $config->find_binary( '/does/not/exist/' . $path ); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is_deeply( $config, \%expected, 'amended config is correct' ); is( $path, 'ls', 'Found correct path to "ls"' ); is( $path, $newpath, 'No change made from find_binary' ); note('Checks on loading configs'); note('empty dir'); $ENV{HOME} = tempdir( CLEANUP => 1 ); $config = App::ClusterSSH::Config->new(); trap { $config->load_configs(); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, undef, 'die message correct' ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, 'Created new configuration file within $HOME/.clusterssh/' . $/, 'Got correct STDERR output for .csshrc' ); #note(qx/ls -laR $ENV{HOME}/); ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' ); ok( -f $ENV{HOME} . '/.clusterssh/config', '.clusterssh config file exists' ); is_deeply( $config, \%expected, 'amended config is correct' ); $ENV{HOME} = undef; note('.csshrc warning'); $ENV{HOME} = tempdir( CLEANUP => 1 ); open( my $csshrc, '>', $ENV{HOME} . '/.csshrc' ); print $csshrc 'auto_quit = no', $/; close($csshrc); $expected{auto_quit} = 'no'; # Note: the load_configs here removes the key_user_x entries delete( $expected{"key_user_$_"} ) for (qw/ 1 2 3 4 /); $config = App::ClusterSSH::Config->new(); trap { $config->load_configs(); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, undef, 'die message correct' ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, 'Moved $HOME/.csshrc to $HOME/.csshrc.DISABLED' . $/ . 'Created new configuration file within $HOME/.clusterssh/' . $/, 'Got correct STDERR output for .csshrc' ); ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' ); ok( -f $ENV{HOME} . '/.clusterssh/config', '.clusterssh config file exists' ); is_deeply( $config, \%expected, 'amended config is correct' ); note('.csshrc warning and .clusterssh dir plus config'); # need to recreate .csshrc as it was just moved open( $csshrc, '>', $ENV{HOME} . '/.csshrc' ); print $csshrc 'auto_quit = no', $/; close($csshrc); $expected{auto_quit} = 'no'; open( $csshrc, '>', $ENV{HOME} . '/.clusterssh/config' ); print $csshrc 'window_tiling = no', $/; close($csshrc); $expected{window_tiling} = 'no'; $config = App::ClusterSSH::Config->new(); trap { $config->load_configs(); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, undef, 'die message correct' ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, 'Moved $HOME/.csshrc to $HOME/.csshrc.DISABLED' . $/, 'Got correct STDERR output for .csshrc' ); ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' ); ok( -f $ENV{HOME} . '/.clusterssh/config', '.clusterssh config file exists' ); is_deeply( $config, \%expected, 'amended config is correct' ); note('no .csshrc warning and .clusterssh dir'); unlink( $ENV{HOME} . '/.csshrc' ); $expected{auto_quit} = 'yes'; $config = App::ClusterSSH::Config->new(); trap { $config->load_configs(); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, undef, 'die message correct' ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, '', 'Expecting no STDERR' ); ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' ); ok( -f $ENV{HOME} . '/.clusterssh/config', '.clusterssh config file exists' ); is_deeply( $config, \%expected, 'amended config is correct' ); note('no .csshrc warning, .clusterssh dir plus config + extra config'); open( $csshrc, '>', $ENV{HOME} . '/clusterssh.config' ); print $csshrc 'terminal_args = something', $/; close($csshrc); $expected{terminal_args} = 'something'; $config = App::ClusterSSH::Config->new(); trap { $config->load_configs( $ENV{HOME} . '/clusterssh.config' ); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, undef, 'die message correct' ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, '', 'Expecting no STDERR' ); ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' ); ok( -f $ENV{HOME} . '/.clusterssh/config', '.clusterssh config file exists' ); is_deeply( $config, \%expected, 'amended config is correct' ); note('no .csshrc warning, .clusterssh dir plus config + more extra configs'); open( $csshrc, '>', $ENV{HOME} . '/.clusterssh/config_ABC' ); print $csshrc 'ssh_args = something', $/; close($csshrc); $expected{ssh_args} = 'something'; $config = App::ClusterSSH::Config->new(); trap { $config->load_configs( $ENV{HOME} . '/clusterssh.config', 'ABC' ); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, undef, 'die message correct' ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, '', 'Expecting no STDERR' ); ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' ); ok( -f $ENV{HOME} . '/.clusterssh/config', '.clusterssh config file exists' ); is_deeply( $config, \%expected, 'amended config is correct' ); note('check .clusterssh file is an error'); $ENV{HOME} = tempdir( CLEANUP => 1 ); open( $csshrc, '>', $ENV{HOME} . '/.clusterssh' ); print $csshrc 'should_be_dir_not_file = PROBLEM', $/; close($csshrc); $config = App::ClusterSSH::Config->new(); trap { $config->write_user_config_file(); }; is( $trap->leaveby, 'die', 'died ok' ); isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, 'Unable to create directory $HOME/.clusterssh: File exists' . $/, 'die message correct' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); note('check failure to write default config is caught'); $ENV{HOME} = tempdir( CLEANUP => 1 ); mkdir( $ENV{HOME} . '/.clusterssh' ); mkdir( $ENV{HOME} . '/.clusterssh/config' ); $config = App::ClusterSSH::Config->new(); trap { $config->write_user_config_file(); }; is( $trap->leaveby, 'die', 'died ok' ); isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, 'Unable to write default $HOME/.clusterssh/config: Is a directory' . $/, 'die message correct' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); note('check .clusterssh errors via load_configs are not fatal'); $ENV{HOME} = tempdir( CLEANUP => 1 ); open( $csshrc, '>', $ENV{HOME} . '/.clusterssh' ); print $csshrc 'should_be_dir_not_file = PROBLEM', $/; close($csshrc); $config = App::ClusterSSH::Config->new(); trap { $config->load_configs(); }; is( $trap->leaveby, 'return', 'died ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{Unable to create directory $HOME/.clusterssh: File exists} . $/ . $/, 'Expecting no STDERR' ); SKIP: { skip "Test inappropriate when running as root", 5 if $< == 0; note('move of .csshrc failure'); $ENV{HOME} = tempdir( CLEANUP => 1 ); open( $csshrc, '>', $ENV{HOME} . '/.csshrc' ); print $csshrc "Something", $/; close($csshrc); open( $csshrc, '>', $ENV{HOME} . '/.csshrc.DISABLED' ); print $csshrc "Something else", $/; close($csshrc); chmod( 0666, $ENV{HOME} . '/.csshrc.DISABLED', $ENV{HOME} ); $config = App::ClusterSSH::Config->new(); trap { $config->write_user_config_file(); }; is( $trap->leaveby, 'die', 'died ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); is( $trap->die, q{Unable to create directory $HOME/.clusterssh: Permission denied} . $/, 'Expected die msg ' . $trap->stderr ); chmod( 0755, $ENV{HOME} . '/.csshrc.DISABLED', $ENV{HOME} ); } note('check failure to write default config is caught when loading config'); $ENV{HOME} = tempdir( CLEANUP => 1 ); mkdir( $ENV{HOME} . '/.clusterssh' ); mkdir( $ENV{HOME} . '/.clusterssh/config' ); $config = App::ClusterSSH::Config->new(); trap { $config->load_configs(); }; is( $trap->leaveby, 'return', 'returned ok' ); isa_ok( $config, "App::ClusterSSH::Config" ); isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->stdout, q{}, 'Expecting no STDOUT' ); is( $trap->stderr, q{Unable to write default $HOME/.clusterssh/config: Is a directory} . $/ . $/, 'Expecting no STDERR' ); note('Checking dump'); $config = App::ClusterSSH::Config->new( send_menu_xml_file => $ENV{HOME} . '/.clusterssh/send_menu', ); trap { $config->dump(); }; my $expected = qq{# Configuration dump produced by "cssh -d" auto_close=5 auto_quit=yes auto_wm_decoration_offsets=no cols=-1 command_post= command_pre= console=console console_args= console_position= external_cluster_command= external_command_mode=0600 external_command_pipe= extra_cluster_file= extra_tag_file= fillscreen=no hide_menu=0 history_height=10 history_width=40 hostname_override= key_addhost=Control-Shift-plus key_clientname=Alt-n key_history=Alt-h key_localname=Alt-l key_macros_enable=Alt-p key_paste=Control-v key_quit=Alt-q key_retilehosts=Alt-r key_user_1=Alt-1 key_user_2=Alt-2 key_user_3=Alt-3 key_user_4=Alt-4 key_username=Alt-u lang=en macro_hostname=%h macro_newline=%n macro_servername=%s macro_user_1=%1 macro_user_1_command= macro_user_2=%2 macro_user_2_command= macro_user_3=%3 macro_user_3_command= macro_user_4=%4 macro_user_4_command= macro_username=%u macro_version=%v macros_enabled=yes max_addhost_menu_cluster_items=6 max_host_menu_items=30 menu_host_autotearoff=0 menu_send_autotearoff=0 mouse_paste=Button-2 rows=-1 rsh=rsh rsh_args= screen_reserve_bottom=60 screen_reserve_left=0 screen_reserve_right=0 screen_reserve_top=0 send_menu_xml_file=} . $ENV{HOME} . qq{/.clusterssh/send_menu sftp=sftp sftp_args= show_history=0 ssh=ssh ssh_args= telnet=telnet telnet_args= terminal=xterm terminal_allow_send_events=-xrm '*.VT100.allowSendEvents:true' terminal_args= terminal_bg_style=dark terminal_colorize=1 terminal_decoration_height=10 terminal_decoration_width=8 terminal_font=6x13 terminal_reserve_bottom=0 terminal_reserve_left=5 terminal_reserve_right=0 terminal_reserve_top=5 terminal_size=80x24 terminal_title_opt=-T unique_servers=0 unmap_on_redraw=no use_all_a_records=0 use_hotkeys=yes use_natural_sort=0 #user= window_tiling=yes window_tiling_direction=right }; isa_ok( $config, "App::ClusterSSH::Config" ); is( $trap->die, undef, 'die message correct' ); eq_or_diff( $trap->stdout, $expected, 'Expecting no STDOUT' ); is( $trap->stderr, q{}, 'Expecting no STDERR' ); done_testing();