The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

#########################

use strict;
use Test::More;
use Data::Dumper;

if ( ! defined $ENV{TEST_SOCKET} or !defined $ENV{TEST_SERVER} ) {
    my $msg = 'Author test.  Set $ENV{TEST_SOCKET} and $ENV{TEST_SERVER} to run';
    plan( skip_all => $msg );
} else {
    plan( tests => 727 );
}

# set an alarm
my $lastquery;
$SIG{ALRM} = sub {
    my @caller = caller;
    print STDERR 'last query: '.$lastquery if defined $lastquery;
    die "timeout reached:".Dumper(\@caller)."\n" 
};
alarm(120);

use_ok('Monitoring::Livestatus');

#########################
my $line_seperator      = 10;
my $column_seperator    = 0;
my $objects_to_test = {
  # UNIX
  # create unix object with a single arg
#  '01 unix_single_arg' => Monitoring::Livestatus::UNIX->new( $ENV{TEST_SOCKET} ),

  # create unix object with hash args
  '02 unix_few_args' => Monitoring::Livestatus->new(
                                      #verbose             => 1,
                                      socket              => $ENV{TEST_SOCKET},
                                      line_seperator      => $line_seperator,
                                      column_seperator    => $column_seperator,
                                    ),

  # create unix object with hash args
  '03 unix_keepalive' => Monitoring::Livestatus->new(
                                      verbose             => 0,
                                      socket              => $ENV{TEST_SOCKET},
                                      keepalive           => 1,
                                    ),

  # TCP
  # create inet object with a single arg
  '04 inet_single_arg' => Monitoring::Livestatus::INET->new( $ENV{TEST_SERVER} ),

  # create inet object with hash args
  '05 inet_few_args' => Monitoring::Livestatus->new(
                                      verbose             => 0,
                                      server              => $ENV{TEST_SERVER},
                                      line_seperator      => $line_seperator,
                                      column_seperator    => $column_seperator,
                                    ),


  # create inet object with keepalive
  '06 inet_keepalive' => Monitoring::Livestatus->new(
                                      verbose             => 0,
                                      server              => $ENV{TEST_SERVER},
                                      keepalive           => 1,
                                    ),

  # create multi single args
  '07 multi_keepalive' => Monitoring::Livestatus->new( [ $ENV{TEST_SERVER}, $ENV{TEST_SOCKET} ] ),

  # create multi object with keepalive
  '08 multi_keepalive_hash_args' => Monitoring::Livestatus->new(
                                      verbose             => 0,
                                      peer                => [ $ENV{TEST_SERVER}, $ENV{TEST_SOCKET} ],
                                      keepalive           => 1,
                                    ),

  # create multi object without keepalive
  '09 multi_no_keepalive' => Monitoring::Livestatus->new(
                                      peer                => [ $ENV{TEST_SERVER}, $ENV{TEST_SOCKET} ],
                                      keepalive           => 0,
                                    ),

  # create multi object without threads
  '10 multi_no_threads' => Monitoring::Livestatus->new(
                                      peer                => [ $ENV{TEST_SERVER}, $ENV{TEST_SOCKET} ],
                                      use_threads         => 0,
                                    ),

  # create multi object with only one peer
  '11 multi_one_peer' => Monitoring::Livestatus::MULTI->new(
                                      peer                => $ENV{TEST_SERVER},
                                    ),

  # create multi object without threads
  '12 multi_two_peers' => Monitoring::Livestatus::MULTI->new(
                                      peer                => [ $ENV{TEST_SERVER}, $ENV{TEST_SOCKET} ],
                                    ),
};

my $expected_keys = {
    'columns'       => [
                         'description','name','table','type'
                       ],
    'commands'      => [
                         'line','name'
                       ],
    'comments'      => [
                         '__all_from_hosts__', '__all_from_services__',
                         'author','comment','entry_time','entry_type','expire_time','expires', 'id','persistent',
                         'source','type'
                       ],
    'contacts'      => [
                         'address1','address2','address3','address4','address5','address6','alias',
                         'can_submit_commands','custom_variable_names','custom_variable_values','email',
                         'host_notification_period','host_notifications_enabled','in_host_notification_period',
                         'in_service_notification_period','name','modified_attributes','modified_attributes_list',
                         'pager','service_notification_period','service_notifications_enabled'
                       ],
    'contactgroups' => [ 'name', 'alias', 'members' ],
    'downtimes'     => [
                         '__all_from_hosts__', '__all_from_services__',
                         'author','comment','duration','end_time','entry_time','fixed','id','start_time',
                         'triggered_by','type'
                       ],
    'hostgroups'    => [
                         'action_url','alias','members','name','members_with_state','notes','notes_url','num_hosts','num_hosts_down',
                         'num_hosts_pending','num_hosts_unreach','num_hosts_up','num_services','num_services_crit',
                         'num_services_hard_crit','num_services_hard_ok','num_services_hard_unknown',
                         'num_services_hard_warn','num_services_ok','num_services_pending','num_services_unknown',
                         'num_services_warn','worst_host_state','worst_service_hard_state','worst_service_state'
                       ],
    'hosts'         => [
                         'accept_passive_checks','acknowledged','acknowledgement_type','action_url','action_url_expanded',
                         'active_checks_enabled','address','alias','check_command','check_freshness','check_interval',
                         'check_options','check_period','check_type','checks_enabled','childs','comments','comments_with_info',
                         'contacts','current_attempt','current_notification_number','custom_variable_names',
                         'custom_variable_values','display_name','downtimes','downtimes_with_info','event_handler_enabled',
                         'execution_time','first_notification_delay','flap_detection_enabled','groups','hard_state','has_been_checked',
                         'high_flap_threshold','icon_image','icon_image_alt','icon_image_expanded','in_check_period',
                         'in_notification_period','initial_state','is_executing','is_flapping','last_check','last_hard_state',
                         'last_hard_state_change','last_notification','last_state','last_state_change','latency','last_time_down',
                         'last_time_unreachable','last_time_up','long_plugin_output','low_flap_threshold','max_check_attempts','name',
                         'modified_attributes','modified_attributes_list','next_check',
                         'next_notification','notes','notes_expanded','notes_url','notes_url_expanded','notification_interval',
                         'notification_period','notifications_enabled','num_services','num_services_crit','num_services_hard_crit',
                         'num_services_hard_ok','num_services_hard_unknown','num_services_hard_warn','num_services_ok',
                         'num_services_pending','num_services_unknown','num_services_warn','obsess_over_host','parents',
                         'pending_flex_downtime','percent_state_change','perf_data','plugin_output',
                         'process_performance_data','retry_interval','scheduled_downtime_depth','services','services_with_state',
                         'state','state_type','statusmap_image','total_services','worst_service_hard_state','worst_service_state',
                         'x_3d','y_3d','z_3d'
                       ],
    'hostsbygroup'  => [
                         '__all_from_hosts__', '__all_from_hostgroups__'
                       ],
    'log'           => [
                         '__all_from_hosts__','__all_from_services__','__all_from_contacts__','__all_from_commands__',
                         'attempt','class','command_name','comment','contact_name','host_name','lineno','message','options',
                         'plugin_output','service_description','state','state_type','time','type'
                       ],
    'servicegroups' => [
                         'action_url','alias','members','name','members_with_state','notes','notes_url','num_services','num_services_crit',
                         'num_services_hard_crit','num_services_hard_ok','num_services_hard_unknown',
                         'num_services_hard_warn','num_services_ok','num_services_pending','num_services_unknown',
                         'num_services_warn','worst_service_state'
                       ],
    'servicesbygroup' => [
                         '__all_from_services__', '__all_from_hosts__', '__all_from_servicegroups__'
                       ],
    'services'      => [
                         '__all_from_hosts__',
                         'accept_passive_checks','acknowledged','acknowledgement_type','action_url','action_url_expanded',
                         'active_checks_enabled','check_command','check_interval','check_options','check_period',
                         'check_type','checks_enabled','comments','comments_with_info','contacts','current_attempt',
                         'current_notification_number','custom_variable_names','custom_variable_values',
                         'description','display_name','downtimes','downtimes_with_info','event_handler','event_handler_enabled',
                         'execution_time','first_notification_delay','flap_detection_enabled','groups',
                         'has_been_checked','high_flap_threshold','icon_image','icon_image_alt','icon_image_expanded','in_check_period',
                         'in_notification_period','initial_state','is_executing','is_flapping','last_check',
                         'last_hard_state','last_hard_state_change','last_notification','last_state',
                         'last_state_change','latency','last_time_critical','last_time_ok','last_time_unknown','last_time_warning',
                         'long_plugin_output','low_flap_threshold','max_check_attempts','modified_attributes','modified_attributes_list',
                         'next_check','next_notification','notes','notes_expanded','notes_url','notes_url_expanded',
                         'notification_interval','notification_period','notifications_enabled','obsess_over_service',
                         'percent_state_change','perf_data','plugin_output','process_performance_data','retry_interval',
                         'scheduled_downtime_depth','state','state_type'
                       ],
    'servicesbyhostgroup' => [
                         '__all_from_services__', '__all_from_hosts__', '__all_from_hostgroups__'
                       ],
    'status'        => [
                         'accept_passive_host_checks','accept_passive_service_checks','cached_log_messages',
                         'check_external_commands','check_host_freshness','check_service_freshness','connections',
                         'connections_rate','enable_event_handlers','enable_flap_detection','enable_notifications',
                         'execute_host_checks','execute_service_checks','forks','forks_rate','host_checks','host_checks_rate','interval_length',
                         'last_command_check','last_log_rotation','livestatus_version','log_messages','log_messages_rate','nagios_pid','neb_callbacks',
                         'neb_callbacks_rate','obsess_over_hosts','obsess_over_services','process_performance_data',
                         'program_start','program_version','requests','requests_rate','service_checks','service_checks_rate'
                       ],
    'timeperiods'   => [ 'in', 'name', 'alias' ],
};

my $author = 'Monitoring::Livestatus test';
for my $key (sort keys %{$objects_to_test}) {
    my $ml = $objects_to_test->{$key};
    isa_ok($ml, 'Monitoring::Livestatus') or BAIL_OUT("no need to continue without a proper Monitoring::Livestatus object: ".$key);

    # dont die on errors
    $ml->errors_are_fatal(0);
    $ml->warnings(0);

    #########################
    # set downtime for a host and service
    my $downtimes = $ml->selectall_arrayref("GET downtimes\nColumns: id");
    my $num_downtimes = 0;
    $num_downtimes = scalar @{$downtimes} if defined $downtimes;
    my $firsthost = $ml->selectscalar_value("GET hosts\nColumns: name\nLimit: 1");
    isnt($firsthost, undef, 'get test hostname') or BAIL_OUT($key.': got not test hostname');
    $ml->do('COMMAND ['.time().'] SCHEDULE_HOST_DOWNTIME;'.$firsthost.';'.time().';'.(time()+300).';1;0;300;'.$author.';perl test: '.$0);
    my $firstservice = $ml->selectscalar_value("GET services\nColumns: description\nFilter: host_name = $firsthost\nLimit: 1");
    isnt($firstservice, undef, 'get test servicename') or BAIL_OUT('got not test servicename');
    $ml->do('COMMAND ['.time().'] SCHEDULE_SVC_DOWNTIME;'.$firsthost.';'.$firstservice.';'.time().';'.(time()+300).';1;0;300;'.$author.';perl test: '.$0);
    # sometimes it takes while till the downtime is accepted
    my $waited = 0;
    while(scalar @{$ml->selectall_arrayref("GET downtimes\nColumns: id")} < $num_downtimes + 2) {
      print "waiting for the downtime...\n";
      sleep(1);
      $waited++;
      BAIL_OUT('waited 30 seconds for the downtime...') if $waited > 30;
    }
    #########################

    #########################
    # check tables
    my $data            = $ml->selectall_hashref("GET columns\nColumns: table", 'table');
    my @tables          = sort keys %{$data};
    my @expected_tables = sort keys %{$expected_keys};
    is_deeply(\@tables, \@expected_tables, $key.' tables') or BAIL_OUT("got tables:\n".join(', ', @tables)."\nbut expected\n".join(', ', @expected_tables));

    #########################
    # check keys
    for my $type (keys %{$expected_keys}) {
        my $filter = "";
        $filter  = "Filter: time > ".(time() - 86400)."\n" if $type eq 'log';
        $filter .= "Filter: time < ".(time())."\n"         if $type eq 'log';
        my $expected_keys = get_expected_keys($type);
        my $statement = "GET $type\n".$filter."Limit: 1";
        $lastquery = $statement;
        my $hash_ref  = $ml->selectrow_hashref($statement );
        undef $lastquery;
        is(ref $hash_ref, 'HASH', $type.' keys are a hash') or BAIL_OUT($type.'keys are not in hash format, got '.Dumper($hash_ref));
        my @keys      = sort keys %{$hash_ref};
        is_deeply(\@keys, $expected_keys, $key.' '.$type.' table columns') or BAIL_OUT("got $type keys:\n".join(', ', @keys)."\nbut expected\n".join(', ', @{$expected_keys}));
    }

    my $statement = "GET hosts\nColumns: name as hostname state\nLimit: 1";
    $lastquery = $statement;
    my $hash_ref  = $ml->selectrow_hashref($statement);
    undef $lastquery;
    isnt($hash_ref, undef, $key.' test column alias');
    is($Monitoring::Livestatus::ErrorCode, 0, $key.' test column alias') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    #########################
    # send a test command
    # commands still dont work and breaks livestatus
    my $rt = $ml->do('COMMAND ['.time().'] SAVE_STATE_INFORMATION');
    is($rt, '1', $key.' test command');

    #########################
    # check for errors
    #$ml->{'verbose'} = 1;
    $statement = "GET hosts\nLimit: 1";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    isnt($hash_ref, undef, $key.' test error 200 body');
    is($Monitoring::Livestatus::ErrorCode, 0, $key.' test error 200 status') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "BLAH hosts";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 401 body');
    is($Monitoring::Livestatus::ErrorCode, '401', $key.' test error 401 status') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nLimit: ";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 403 body');
    is($Monitoring::Livestatus::ErrorCode, '403', $key.' test error 403 status') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET unknowntable\nLimit: 1";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 404 body');
    is($Monitoring::Livestatus::ErrorCode, '404', $key.' test error 404 status') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nColumns: unknown";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 405 body');
    TODO: {
        local $TODO = 'livestatus returns wrong status';
        is($Monitoring::Livestatus::ErrorCode, '405', $key.' test error 405 status') or
            diag('got error: '.$Monitoring::Livestatus::ErrorMessage);
    };

    #########################
    # some more broken statements
    $statement = "GET ";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement);
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 403 body');
    is($Monitoring::Livestatus::ErrorCode, '403', $key.' test error 403 status: GET ') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nColumns: name, name";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 405 body');
    is($Monitoring::Livestatus::ErrorCode, '405', $key.' test error 405 status: GET hosts\nColumns: name, name') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nColumns: ";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 405 body');
    is($Monitoring::Livestatus::ErrorCode, '405', $key.' test error 405 status: GET hosts\nColumns: ') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    #########################
    # some forbidden headers
    $statement = "GET hosts\nKeepAlive: on";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 496 body');
    is($Monitoring::Livestatus::ErrorCode, '496', $key.' test error 496 status: KeepAlive: on') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nResponseHeader: fixed16";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 495 body');
    is($Monitoring::Livestatus::ErrorCode, '495', $key.' test error 495 status: ResponseHeader: fixed16') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nColumnHeaders: on";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 494 body');
    is($Monitoring::Livestatus::ErrorCode, '494', $key.' test error 494 status: ColumnHeader: on') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nOuputFormat: json";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 493 body');
    is($Monitoring::Livestatus::ErrorCode, '493', $key.' test error 493 status: OutputForma: json') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);

    $statement = "GET hosts\nSeparators: 0 1 2 3";
    $lastquery = $statement;
    $hash_ref  = $ml->selectrow_hashref($statement );
    undef $lastquery;
    is($hash_ref, undef, $key.' test error 492 body');
    is($Monitoring::Livestatus::ErrorCode, '492', $key.' test error 492 status: Seperators: 0 1 2 3') or
        diag('got error: '.$Monitoring::Livestatus::ErrorMessage);


    #########################
    # check some fancy stats queries
    my $stats_query = "GET services
Stats: state = 0 as all_ok
Stats: state = 1 as all_warning
Stats: state = 2 as all_critical
Stats: state = 3 as all_unknown
Stats: state = 4 as all_pending
Stats: host_state != 0
Stats: state = 1
StatsAnd: 2 as all_warning_on_down_hosts
Stats: host_state != 0
Stats: state = 2
StatsAnd: 2 as all_critical_on_down_hosts
Stats: host_state != 0
Stats: state = 3
StatsAnd: 2 as all_unknown_on_down_hosts
Stats: host_state != 0
Stats: state = 3
Stats: active_checks_enabled = 1
StatsAnd: 3 as all_unknown_active_on_down_hosts
Stats: state = 3
Stats: active_checks_enabled = 1
StatsOr: 2 as all_active_or_unknown";
    $lastquery = $stats_query;
    $hash_ref  = $ml->selectrow_hashref($stats_query );
    undef $lastquery;
    isnt($hash_ref, undef, $key.' test fancy stats query') or
        diag('got error: '.Dumper($hash_ref));
}



# generate expected keys
sub get_expected_keys {
    my $type = shift;
    my $skip = shift;
    my @keys = @{$expected_keys->{$type}};

    my @new_keys;
    for my $key (@keys) {
        my $replaced = 0;
        for my $replace_with (keys %{$expected_keys}) {
            if($key eq '__all_from_'.$replace_with.'__') {
                $replaced = 1;
                next if $skip;
                my $prefix = $replace_with.'_';
                if($replace_with eq "hosts")         { $prefix = 'host_';    }
                if($replace_with eq "services")      { $prefix = 'service_'; }
                if($replace_with eq "commands")      { $prefix = 'command_'; }
                if($replace_with eq "contacts")      { $prefix = 'contact_'; }
                if($replace_with eq "servicegroups") { $prefix = 'servicegroup_'; }
                if($replace_with eq "hostgroups")    { $prefix = 'hostgroup_'; }

                if($type eq "log") { $prefix = 'current_'.$prefix; }

                if($type eq "servicesbygroup"     and $replace_with eq 'services')   { $prefix = ''; }
                if($type eq "servicesbyhostgroup" and $replace_with eq 'services')   { $prefix = ''; }
                if($type eq "hostsbygroup"        and $replace_with eq 'hosts')      { $prefix = ''; }

                my $replace_keys = get_expected_keys($replace_with, 1);
                for my $key2 (@{$replace_keys}) {
                    push @new_keys, $prefix.$key2;
                }
            }
        }
        if($replaced == 0) {
            push @new_keys, $key;
        }
    }

    # has been fixed in 1.1.1rc
    #if($type eq 'log') {
    #  my %keys = map { $_ => 1 } @new_keys;
    #  delete $keys{'current_contact_can_submit_commands'};
    #  delete $keys{'current_contact_host_notifications_enabled'};
    #  delete $keys{'current_contact_in_host_notification_period'};
    #  delete $keys{'current_contact_in_service_notification_period'};
    #  delete $keys{'current_contact_service_notifications_enabled'};
    #  @new_keys = keys %keys;
    #}

    my @return = sort @new_keys;
    return(\@return);
}