diff --git a/lib/NGCP/Panel/Controller/API/MetaConfigDefs.pm b/lib/NGCP/Panel/Controller/API/MetaConfigDefs.pm new file mode 100644 index 0000000000..a7e20e07b9 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/MetaConfigDefs.pm @@ -0,0 +1,161 @@ +package NGCP::Panel::Controller::API::MetaConfigDefs; +use NGCP::Panel::Utils::Generic qw(:all); +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Preferences; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +use JSON::Types qw(); +use Config::General; +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +sub allowed_methods{ + return [qw/GET OPTIONS HEAD/]; +} + +use base qw/Catalyst::Controller NGCP::Panel::Role::API/; + +sub resource_name{ + return 'metaconfigdefs'; +} +sub dispatch_path{ + return '/api/metaconfigdefs/'; +} +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-metaconfigdefs'; +} + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/],#left adminand reseller, as test can run as reseller too. Just don't return full config + Args => 0, + Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods } + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +sub GET :Allow { + my ($self, $c) = @_; + { + my @links; + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf('%s', $self->dispatch_path)); + + my $hal = Data::HAL->new( + links => [@links], + ); + + #my $resource = $c->config; + my $catalyst_config = Config::General->new($c->config->{'Plugin::ConfigLoader'}->{file}); + my %config_internal = $catalyst_config->getall(); + my %config; + + $config{file} = $c->config->{'Plugin::ConfigLoader'}->{file}; + $config{numbermanagement}->{auto_sync_cli} = $config_internal{numbermanagement}->{auto_sync_cli}; + $config{numbermanagement}->{auto_allow_cli} = $config_internal{numbermanagement}->{auto_allow_cli}; + $config{features} = $config_internal{features}; + + my $meta = { + collections => { + #name => { + #module => '', + #allowed_methods => [], + #module_item =>'' + #allowed_methods_item => [], + #query_params => [], + #allowed_roles => [], + #container_item_id => '', + #unique_fields => [['table.field','table1.fields1']], + #} + }, + }; + (my($files,$modules,$collections)) = NGCP::Panel::Utils::API::get_collections('NGCP::Panel::Controller::API::MetaConfigDefs'); + + for ( my $i=0; $i < $#$collections; $i++) + { + my $collection = $collections->[$i]; + my $module = $modules->[$i]; + my $module_item = $module.'Item'; + my $roles = $module->can('config') ? $module->config->{action}->{OPTIONS}->{AllowedRole}:[]; + (!(ref $roles eq 'ARRAY')) and $roles = [$roles]; + $meta->{collections}->{$collection} = { + module => $module, + allowed_methods => $module->can('config') ? $module->config->{action} : {}, + query_params => $module->can('query_params') ? [map {$_->{param}} @{$module->query_params}] : [], + allowed_roles => [$roles], + module_item => $module_item->can('config') ? $module_item : '', + allowed_methods_item => $module_item->can('config') ? $module_item->config->{action} : {}, + #container_item_id => '', + #unique_fields => [['table.field','table1.fields1']], + }; + } + + + my $resource = { config => \%config, meta => $meta }; + $hal->resource($resource); + + my $response = HTTP::Response->new(HTTP_OK, undef, + HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/Root.pm b/lib/NGCP/Panel/Controller/API/Root.pm index 6c7ad12db1..11c5d7f7c8 100644 --- a/lib/NGCP/Panel/Controller/API/Root.pm +++ b/lib/NGCP/Panel/Controller/API/Root.pm @@ -11,6 +11,7 @@ use HTTP::Status qw(:constants); use File::Find::Rule; use JSON qw(to_json); use Safe::Isa qw($_isa); +use NGCP::Panel::Utils::API; use parent qw/Catalyst::Controller NGCP::Panel::Role::API/; require Catalyst::ActionRole::ACL; require Catalyst::ActionRole::CheckTrailingSlash; @@ -59,10 +60,11 @@ sub GET : Allow { "PeeringServerPreferenceDefs" => 1, "PbxDevicePreferenceDefs" => 1, "PbxDeviceProfilePreferenceDefs" => 1, + "MetaConfigDefs" => 1, }; - my @colls = $self->get_collections; - foreach my $coll(@colls) { + my $colls = NGCP::Panel::Utils::API::get_collections_files; + foreach my $coll(@$colls) { my $mod = $coll; $mod =~ s/^.+\/([a-zA-Z0-9_]+)\.pm$/$1/; next if(exists $blacklist->{$mod}); @@ -224,36 +226,14 @@ sub OPTIONS : Allow { return; } -sub get_collections { - my ($self) = @_; - - # figure out base path of our api modules - my $libpath = $INC{"NGCP/Panel/Controller/API/Root.pm"}; - $libpath =~ s/Root\.pm$//; - - # find all modules not called Root.pm and *Item.pm - # (which should then be just collections) - my $rootrule = File::Find::Rule->new->name('Root.pm'); - my $itemrule = File::Find::Rule->new->name('*Item.pm'); - my $rule = File::Find::Rule->new - ->mindepth(1) - ->maxdepth(1) - ->name('*.pm') - ->not($rootrule) - ->not($itemrule); - my @colls = $rule->in($libpath); - - return @colls; -} - sub collections_link_headers : Private { my ($self) = @_; - my @colls = $self->get_collections; + my $colls = NGCP::Panel::Utils::API::get_collections_files; # create Link header for each of the collections my @links = (); - foreach my $mod(@colls) { + foreach my $mod(@$colls) { # extract file base from path (e.g. Foo from lib/something/Foo.pm) $mod =~ s/^.+\/([a-zA-Z0-9_]+)\.pm$/$1/; my $rel = lc $mod; diff --git a/lib/NGCP/Panel/Utils/API.pm b/lib/NGCP/Panel/Utils/API.pm new file mode 100644 index 0000000000..c6c00dac09 --- /dev/null +++ b/lib/NGCP/Panel/Utils/API.pm @@ -0,0 +1,68 @@ +package NGCP::Panel::Utils::API; +use strict; +use warnings; + +use File::Find::Rule; + + +sub get_collections { + my @files = @{get_collections_files()}; + my(@collections, @packages, @modules); + foreach my $mod(@files) { + # extract file base from path (e.g. Foo from lib/something/Foo.pm) + $mod =~ s/^.+\/([a-zA-Z0-9_]+)\.pm$/$1/; + my $package = 'NGCP::Panel::Controller::API::'.$mod; + my $rel = lc $mod; + $mod = 'NGCP::Panel::Controller::API::'.$mod; + push @modules, $mod; + push @packages, $package; + push @collections, $rel; + } + return \@files, \@packages, \@collections, \@modules; +} +sub get_collections_files { + my($library,$libpath) = @_; + if(!$libpath){ + # figure out base path of our api modules + $library ||= "NGCP/Panel/Controller/API/Root.pm"; + $libpath = $INC{$library}; + $libpath =~ s/\/[^\/]+$/\//; + } + # find all modules not called Root.pm and *Item.pm + # (which should then be just collections) + my $rootrule = File::Find::Rule->new->name('Root.pm'); + my $itemrule = File::Find::Rule->new->name('*Item.pm'); + my $rule = File::Find::Rule->new + ->mindepth(1) + ->maxdepth(1) + ->name('*.pm') + ->not($rootrule) + ->not($itemrule); + my @colls = $rule->in($libpath); + + return \@colls; +} + +1; + +=head1 NAME + +NGCP::Panel::Utils::API + +=head1 DESCRIPTION + +A helper to manipulate REST API related data + +=head1 METHODS + +=head1 AUTHOR + +Irina Peshinskaya + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Utils/API/Subscribers.pm b/lib/NGCP/Panel/Utils/API/Subscribers.pm index 50a0305da3..d205794e8c 100644 --- a/lib/NGCP/Panel/Utils/API/Subscribers.pm +++ b/lib/NGCP/Panel/Utils/API/Subscribers.pm @@ -35,7 +35,7 @@ NGCP::Panel::Utils::API::Subscribers =head1 DESCRIPTION -A temporary helper to manipulate subscribers related data in REST API moules +A temporary helper to manipulate subscribers related data in REST API modules =head1 METHODS diff --git a/sandbox/inheritance.pl b/sandbox/inheritance.pl new file mode 100644 index 0000000000..27e537d242 --- /dev/null +++ b/sandbox/inheritance.pl @@ -0,0 +1,26 @@ +package Parent; + +our $VERSION = 1.23; + +sub VERSION { $VERSION } + +sub child_version { $_[0]->VERSION } + +package Child; +use base qw(Parent); + +our $VERSION = 5.43; + +sub VERSION { $VERSION } + +sub new { bless {}, $_[0]; } + +sub parent_version { $_[0]->SUPER::VERSION } + + +print "Child version is ", Child->VERSION, "\n"; # 5.43 +my $child = Child->new; + +print "Child version: ", $child->VERSION, "\n"; # 5.43 +print "Parent version: ", $child->parent_version, "\n"; # 1.23 +print "Child version: ", $child->child_version, "\n"; # 5.43 \ No newline at end of file diff --git a/t/api-rest/api-all.t b/t/api-rest/api-all.t new file mode 100644 index 0000000000..a43a17ee57 --- /dev/null +++ b/t/api-rest/api-all.t @@ -0,0 +1,153 @@ +use strict; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; +use Getopt::Long; +use File::Find::Rule; +use File::Basename; +use Clone qw/clone/; + +my $opt = { + 'collections' => {}, +}; +my $opt_in = {}; +GetOptions($opt_in, + "help|h|?" , + "collections:s" , + "ignore-existence" , +) or pod2usage(2); +my @opt_keys = keys %$opt_in; +@{$opt}{ map{my $k=$_;$k=~s/\-/_/;$k;} @opt_keys } = map {my $v = $opt_in->{$_}; $v={ map {$_=>1;} split(/[^[:alnum:]]+/,$v ) }; $v;} @opt_keys ; +print Dumper $opt; +pod2usage(1) if $opt->{help}; +pod2usage(1) unless( 1 +# defined $opt->{collections} && defined $opt->{etc} +); + + +my $test_machine = Test::Collection->new('name'=>'','ALLOW_EMPTY_COLLECTION' => 1); +my $fake_data = Test::FakeData->new; +$test_machine->clear_cache; +my $remote_config = $test_machine->init_catalyst_config; +print Dumper $remote_config ; +my $data = $remote_config->{meta}->{'collections'}; + + +my %test_exclude = ( + 'metaconfigdefs' => 1, + 'subscriberpreferencedefs' => 1, + 'customerpreferencedefs' => 1, + 'domainpreferencedefs' => 1, + 'peeringserverpreferencedefs' => 1, + 'profilepreferencedefs' => 1, + 'subscriberpreferences' => 1, + 'customerpreferences' => 1, + 'domainpreferences' => 1, + 'peeringserverpreferences' => 1, + 'profilepreferences' => 1, + 'pbxdevicepreferencedefs' => 1, + 'pbxdeviceprofilepreferencedefs' => 1, + #defs and preferences are tested in context of preferences + 'pbxdevicefirmwares' => 1, #too hard, fails with timeout on get +#falis with: not ok 163 - ccmapentries: check_get2put: check put successful (Unprocessable Entity: Validation failed. field='mappings', input='ARRAY(0x1a53f278)', errors='Mappings field is required') + 'ccmapentries' => 1, +#fails with: +# got: 'https://127.0.0.1:1443/api/customerzonecosts/?page=1&rows=5&start=2016-10-01T000000&end=2016-10-31T235959' +# expected: 'https://127.0.0.1:1443/api/customerzonecosts/?page=1&rows=5' + 'customerzonecosts' => 1, +#fails with: Unsupported media type 'application/json', accepting text/plain or text/xml only.) + 'pbxdeviceconfigs' => 1, +#fails with: not ok 1131 - rtcapps: check_get2put: check put successful (Unprocessable Entity: Invalid field 'apps'. Must be an array.) + 'rtcapps' => 1, +#fails with: not ok 1176 - rtcnetworks: check_get2put: check put successful (Unprocessable Entity: Invalid field 'networks'. Must be an array.) + 'rtcnetworks' => 1, +); +my %test_exists; +{ + my $dir = dirname($0); + my $rule = File::Find::Rule->new + ->mindepth(1) + ->maxdepth(1) + ->name('api-*.t'); + %test_exists = map {$_=~s/\Q$dir\/\E//;$_ => 1} $rule->in($dir); +} + +my $res = { + 'collections_no_get' => [], + 'collections_empty' => [], + 'collections_not_empty' => [], + 'strange_item_actions' => {}, + 'no_module_item' => [], + 'tests_exists' => \%test_exists, + 'tests_exists_skipped' => [], + 'checked' => [], + 'tests_exclude' => \%test_exclude, + 'opt' => $opt +}; +foreach my $collection ( sort grep{(! ( scalar keys $opt->{collections} ) ) || $opt->{collections}->{$_} } keys %{$data} ){ + if(!$opt->{collections}->{$collection}){ + if($test_exists{'api-'.$collection.'.t'} && !$opt->{ignore_existence}){ + push @{$res->{'tests_exists_skipped'}}, $collection; + #we will not test the same twice + next; + } + next if $test_exclude{$collection}; + } + + #print Dumper $data->{$collection}->{allowed_methods_item}; + #print Dumper $collection; + + my $item_allowed_actions = { allowed => {} }; + if($data->{$collection}->{module_item}){ + if(ref $data->{$collection}->{allowed_methods_item} eq 'HASH'){ + $item_allowed_actions = { allowed => { map { $_ => 1 } keys %{$data->{$collection}->{allowed_methods_item}} }}; + }else{ + $res->{'strange_item_actions'}->{$collection} = $data->{$collection}->{allowed_methods_item}; + } + }else{ + push @{$res->{'no_module_item'}}, $collection; + } + push @{$res->{'checked'}}, $collection; + $test_machine->name($collection); + $test_machine->NO_ITEM_MODULE($data->{$collection}->{module_item} ? 0 : 1 ); + { + $test_machine->methods({ + collection => { allowed => { map { $_ => 1 } keys %{$data->{$collection}->{allowed_methods}} }}, + item => $item_allowed_actions, + }); + } + + $test_machine->check_bundle(); + if($test_machine->{methods}->{collection}->{allowed}->{GET}){ + my $item = $test_machine->get_item_hal(); + #if($item->{content}->{total_count}){ + if(!$test_machine->IS_EMPTY_COLLECTION){ + push @{$res->{'collections_not_empty'}}, $collection; + if($data->{$collection}->{allowed_methods}->{POST}){ + my $item_post = clone($item); + delete $item_post->{content}->{id}; + $test_machine->DATA_ITEM_STORE($item_post->{content}); + $test_machine->form_data_item(); + #test_machine->check_create_correct( 1 ); + } + if($test_machine->{methods}->{item}->{allowed}->{PUT}){ + $test_machine->check_get2put(); + } + }else{ + push @{$res->{'collections_empty'}}, $collection; + } + }else{ + push @{$res->{'collections_no_get'}}, $collection; + } +} + +$test_machine->clear_test_data_all(); +done_testing; + +undef $fake_data; +undef $test_machine; + +print Dumper $res; +# vim: set tabstop=4 expandtab: \ No newline at end of file diff --git a/t/api-rest/api-faxes.t b/t/api-rest/api-faxes.t index ccaa46441e..66439704fb 100644 --- a/t/api-rest/api-faxes.t +++ b/t/api-rest/api-faxes.t @@ -34,53 +34,46 @@ my $test_machine = Test::Collection->new( embedded_resources => [qw/subscribers/] ); -{ - my ($res, $content, $req) = $test_machine->check_item_post(); - if(422 == $res->code){ - $test_machine->http_code_msg(422, "check faxserver feature state: disabled", $res, $content); - my $inactive_feature_msg = "Faxserver feature is not active"; - if( $content->{message} =~ /$inactive_feature_msg/ ){ - #some weird construction of the tests, but in case of inactive faxes feature and inactive faxes for the userboth response code will be 422. - #if feature is inactive on the application level - there is nothing to test more - #so added this pseudo test just to place it here. Really don't like it. - ok($content->{message} =~ /$inactive_feature_msg/, "check error message in body: $inactive_feature_msg"); - done_testing; - exit(); - } - } -} +my $remote_config = $test_machine->init_catalyst_config; +if( !$remote_config->{config}->{features}->{faxserver} ){ + $remote_config->{config}->{features}->{faxserver} //= 0; + is($remote_config->{config}->{features}->{faxserver},0,"axserver feature isn't enabled"); +}else{ + @{$test_machine->content_type}{qw/POST PUT/} = (('multipart/form-data') x 2); + $test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; + $test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS)}; + $test_machine->DATA_ITEM_STORE($fake_data->process('faxes')); + $test_machine->form_data_item(); -@{$test_machine->content_type}{qw/POST PUT/} = (('multipart/form-data') x 2); -$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; -$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS)}; -$test_machine->DATA_ITEM_STORE($fake_data->process('faxes')); -$test_machine->form_data_item(); + { + my $test_machine_aux = Test::Collection->new(name => 'faxserversettings'); + my $uri = $test_machine_aux->get_uri($test_machine->DATA_ITEM->{json}->{subscriber_id}); + my($res,$faxserversettings,$req) = $test_machine_aux->check_item_get($uri); + $faxserversettings->{active} = 1; + $faxserversettings->{password} = 'aaa111'; + $test_machine_aux->request_put($faxserversettings,$uri); + } + $test_machine->resource_fill_file($test_machine->DATA_ITEM->{faxfile}->[0]); + $test_machine->check_create_correct( 1 ); + $test_machine->resource_clear_file($test_machine->DATA_ITEM->{faxfile}->[0]); -{ - my $test_machine_aux = Test::Collection->new(name => 'faxserversettings'); - my $uri = $test_machine_aux->get_uri($test_machine->DATA_ITEM->{json}->{subscriber_id}); - my($res,$faxserversettings,$req) = $test_machine_aux->check_item_get($uri); - $faxserversettings->{active} = 1; - $faxserversettings->{password} = 'aaa111'; - $test_machine_aux->request_put($faxserversettings,$uri); + delete $test_machine->DATA_ITEM->{faxfile}; + $test_machine->DATA_ITEM->{json}->{data}="äöüß"; + $test_machine->form_data_item(); + $test_machine->check_create_correct( 1 ); } -$test_machine->resource_fill_file($test_machine->DATA_ITEM->{faxfile}->[0]); -$test_machine->check_create_correct( 1 ); -$test_machine->resource_clear_file($test_machine->DATA_ITEM->{faxfile}->[0]); - -delete $test_machine->DATA_ITEM->{faxfile}; -$test_machine->DATA_ITEM->{json}->{data}="äöüß"; -$test_machine->form_data_item(); -$test_machine->check_create_correct( 1 ); - #$test_machine->check_bundle(); #$test_machine->check_get2put( sub { $_[0] = { json => JSON::to_json($_[0]), 'faxfile' => $test_machine->DATA_ITEM_STORE->{faxfile} }; } ); +$test_machine->clear_test_data_all(); done_testing; +undef $fake_data; +undef $test_machine; + # vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-root.t b/t/api-rest/api-root.t index a05c75cc4d..79b6bc654d 100644 --- a/t/api-rest/api-root.t +++ b/t/api-rest/api-root.t @@ -87,6 +87,7 @@ $ua->credentials($netloc, "api_admin_http", $user, $pass); lnpnumbers => 1, mailtofaxsettings => 1, maliciouscalls => 1, + metaconfigdefs => 1, ncoslevels => 1, ncoslnpcarriers => 1, ncospatterns => 1, diff --git a/t/lib/Test/Collection.pm b/t/lib/Test/Collection.pm index 73687ccc2b..7d9ae63f28 100644 --- a/t/lib/Test/Collection.pm +++ b/t/lib/Test/Collection.pm @@ -18,6 +18,7 @@ use Test::HTTPRequestAsCurl; use Data::Dumper; use File::Slurp qw/write_file/; use Storable; +use Carp qw(cluck longmess shortmess); has 'data_cache_file' => ( is => 'ro', @@ -47,6 +48,21 @@ has 'DEBUG_ONLY' => ( isa => 'Bool', default => 0, ); +has 'ALLOW_EMPTY_COLLECTION' => ( + is => 'rw', + isa => 'Bool', + default => 0, +); +has 'IS_EMPTY_COLLECTION' => ( + is => 'rw', + isa => 'Bool', + default => 0, +); +has 'NO_ITEM_MODULE' => ( + is => 'rw', + isa => 'Bool', + default => 0, +); has 'catalyst_config' => ( is => 'rw', isa => 'HashRef', @@ -198,30 +214,26 @@ sub get_cloned{ } sub init_catalyst_config{ my $self = shift; - my $catalyst_config; - my $panel_config; - if ($self->{local_test}) { - for my $path(qw#../ngcp_panel.conf ngcp_panel.conf#) { - if(-f $path) { - $panel_config = $path; - last; - } + my $config; + my $restored; + if($self->cache_data){ + $restored = $self->get_cached_data; + if($restored->{loaded} && $restored->{loaded}->{metaconfigdefs}){ + $config = $restored->{loaded}->{metaconfigdefs}->{content}; } - $panel_config //= dirname($0).'/../../ngcp_panel.conf'; - } else { - #taken 1:1 from /lib/NGCP/Panel.pm - for my $path(qw#/etc/ngcp-panel/ngcp_panel.conf etc/ngcp_panel.conf ngcp_panel.conf#) { - if(-f $path) { - $panel_config = $path; - last; - } + } + if(!$config){ + my($res,$list_collection,$req) = $self->check_item_get($self->normalize_uri('/api/metaconfigdefs/')); + my $location; + ($config,$location) = $self->get_hal_from_collection($list_collection); + if($self->cache_data){ + $restored->{loaded} //= {}; + $restored->{loaded}->{metaconfigdefs} = { content => $config, location => $location }; + store $restored, $self->data_cache_file; } - $panel_config //= dirname($0).'/ngcp_panel.conf'; } - $catalyst_config = Config::General->new($panel_config); - my %config = $catalyst_config->getall(); - $self->{catalyst_config} = \%config; - $self->{panel_config} = $panel_config; + $self->{catalyst_config} = $config; + $self->{panel_config} = $config->{file}; return $self->{catalyst_config}; } sub init_ua { @@ -336,12 +348,16 @@ sub get_item_hal{ if(!$resitem){ my ($reshal, $location,$total_count); $uri //= $self->get_uri_collection($name)."?page=1&rows=1"; + #print "uri=$uri;"; my($res,$list_collection,$req) = $self->check_item_get($self->normalize_uri($uri)); ($reshal,$location,$total_count) = $self->get_hal_from_collection($list_collection,$name); if($total_count || ('HASH' eq ref $reshal->{content} && $reshal->{content}->{total_count})){ + $self->IS_EMPTY_COLLECTION(0); $resitem = { num => 1, content => $reshal, res => $res, req => $req, location => $location, total_count => $total_count }; $self->DATA_LOADED->{$name} ||= []; push @{$self->DATA_LOADED->{$name}}, $resitem; + }else{ + $self->IS_EMPTY_COLLECTION(1); } } return $resitem; @@ -553,7 +569,7 @@ sub get_response_content{ } sub normalize_uri{ my($self,$uri) = @_; - $uri ||= $self->get_uri_current; + $uri ||= $self->get_uri_current // ''; if($uri !~/^http/i){ $uri = $self->base_uri.$uri; } @@ -570,30 +586,32 @@ sub check_options_collection{ $uri //= $self->get_uri_collection; my $req = HTTP::Request->new('OPTIONS', $uri ); my $res = $self->request($req); - is($res->header('Accept-Post'), "application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-".$self->name, "check Accept-Post header in options response"); + is($res->header('Accept-Post'), "application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-".$self->name, "$self->{name}: check Accept-Post header in options response"); $self->check_methods($res,'collection'); } sub check_options_item{ my ($self,$uri) = @_; # OPTIONS tests $uri ||= $self->get_uri_current; - my $req = HTTP::Request->new('OPTIONS', $uri); - my $res = $self->request($req); - $self->check_methods($res,'item'); + if(!$self->IS_EMPTY_COLLECTION){ + my $req = HTTP::Request->new('OPTIONS', $uri); + my $res = $self->request($req); + $self->check_methods($res,'item'); + } } sub check_methods{ my($self, $res, $area) = @_; my $opts = $self->get_response_content($res); $self->http_code_msg(200, "check $area options request", $res,$opts); my @hopts = split /\s*,\s*/, $res->header('Allow'); - ok(exists $opts->{methods} && ref $opts->{methods} eq "ARRAY", "check for valid 'methods' in body"); + ok(exists $opts->{methods} && ref $opts->{methods} eq "ARRAY", "$self->{name}: check for valid 'methods' in body"); foreach my $opt(keys %{$self->methods->{$area}->{all}} ) { if(exists $self->methods->{$area}->{allowed}->{$opt}){ - ok(grep(/^$opt$/, @hopts), "check for existence of '$opt' in Allow header"); - ok(grep(/^$opt$/, @{ $opts->{methods} }), "check for existence of '$opt' in body"); + ok(grep(/^$opt$/, @hopts), "$self->{name}: check for existence of '$opt' in Allow header"); + ok(grep(/^$opt$/, @{ $opts->{methods} }), "$self->{name}: check for existence of '$opt' in body"); }else{ - ok(!grep(/^$opt$/, @hopts), "check for absence of '$opt' in Allow header"); - ok(!grep(/^$opt$/, @{ $opts->{methods} }), "check for absence of '$opt' in body"); + ok(!grep(/^$opt$/, @hopts), "$self->{name}: check for absence of '$opt' in Allow header"); + ok(!grep(/^$opt$/, @{ $opts->{methods} }), "$self->{name}: check for absence of '$opt' in body"); } } } @@ -606,25 +624,26 @@ sub check_list_collection{ #print "nexturi=$nexturi;\n"; my ($res,$list_collection) = $self->check_item_get($nexturi); my $selfuri = $self->normalize_uri($list_collection->{_links}->{self}->{href}); - is($selfuri, $nexturi, "check _links.self.href of collection"); + is($selfuri, $nexturi, "$self->{name}: check _links.self.href of collection"); my $colluri = URI->new($selfuri); - - ok($list_collection->{total_count} > 0, "check 'total_count' of collection"); + if(($list_collection->{total_count} && $list_collection->{total_count} > 0 ) || !$self->ALLOW_EMPTY_COLLECTION){ + ok($list_collection->{total_count} > 0, "$self->{name}: check 'total_count' of collection"); + } my %q = $colluri->query_form; - ok(exists $q{page} && exists $q{rows}, "check existence of 'page' and 'row' in 'self'"); + ok(exists $q{page} && exists $q{rows}, "$self->{name}: check existence of 'page' and 'row' in 'self'"); my $page = int($q{page}); my $rows = int($q{rows}); ok($rows != 0, "check existance of the 'rows'"); if($page == 1) { - ok(!exists $list_collection->{_links}->{prev}->{href}, "check absence of 'prev' on first page"); + ok(!exists $list_collection->{_links}->{prev}->{href}, "$self->{name}: check absence of 'prev' on first page"); } else { - ok(exists $list_collection->{_links}->{prev}->{href}, "check existence of 'prev'"); + ok(exists $list_collection->{_links}->{prev}->{href}, "$self->{name}: check existence of 'prev'"); } if(($rows != 0) && ($list_collection->{total_count} / $rows) <= $page) { - ok(!exists $list_collection->{_links}->{next}->{href}, "check absence of 'next' on last page"); + ok(!exists $list_collection->{_links}->{next}->{href}, "$self->{name}: check absence of 'next' on last page"); } else { - ok(exists $list_collection->{_links}->{next}->{href}, "check existence of 'next'"); + ok(exists $list_collection->{_links}->{next}->{href}, "$self->{name}: check existence of 'next'"); } if($list_collection->{_links}->{next}->{href}) { @@ -634,8 +653,10 @@ sub check_list_collection{ } my $hal_name = $self->get_hal_name; - ok(((ref $list_collection->{_links}->{$hal_name} eq "ARRAY" ) || - (ref $list_collection->{_links}->{$hal_name} eq "HASH" ) ), "check if 'ngcp:".$self->name."' is array/hash-ref"); + if(($list_collection->{total_count} && $list_collection->{total_count} > 0 ) || !$self->ALLOW_EMPTY_COLLECTION){ + ok(((ref $list_collection->{_links}->{$hal_name} eq "ARRAY" ) || + (ref $list_collection->{_links}->{$hal_name} eq "HASH" ) ), "$self->{name}: check if 'ngcp:".$self->name."' is array/hash-ref"); + } # it is really strange - we check that the only element of the _links will be hash - and after this treat _embedded as hash too @@ -665,7 +686,7 @@ sub check_created_listed{ foreach (@$listed){ delete $created_items->{$_}; } - is(scalar(keys %{$created_items}), 0, "check if all created test items have been foundin the list"); + is(scalar(keys %{$created_items}), 0, "$self->{name}: check if all created test items have been foundin the list"); if(scalar(keys %{$created_items})){ print Dumper $created_items; print Dumper $listed; @@ -676,7 +697,7 @@ sub check_embedded { my($self, $embedded, $check_embedded_cb) = @_; defined $check_embedded_cb and $check_embedded_cb->($embedded); foreach my $embedded_name(@{$self->embedded_resources}){ - ok(exists $embedded->{_links}->{'ngcp:'.$embedded_name}, "check presence of ngcp:$embedded_name relation"); + ok(exists $embedded->{_links}->{'ngcp:'.$embedded_name}, "$self->{name}: check presence of ngcp:$embedded_name relation"); } } @@ -736,8 +757,8 @@ sub check_patch_correct{ my($self,$content) = @_; my ($res,$rescontent,$req) = $self->request_patch( $content ); $self->http_code_msg(200, "check patched item", $res, $rescontent); - is($rescontent->{_links}->{self}->{href}, $self->uri2location($req->uri), "check patched self link"); - is($rescontent->{_links}->{collection}->{href}, '/api/'.$self->name.'/', "check patched collection link"); + is($rescontent->{_links}->{self}->{href}, $self->uri2location($req->uri), "$self->{name}: check patched self link"); + is($rescontent->{_links}->{collection}->{href}, '/api/'.$self->name.'/', "$self->{name}: check patched collection link"); return ($res,$rescontent,$req); } @@ -770,7 +791,7 @@ sub check_patch_body_empty{ my($self) = @_; my ($res,$content,$req) = $self->request_patch; $self->http_code_msg(400, "check patch missing body", $res, $content); - like($content->{message}, qr/is missing a message body/, "check patch missing body response"); + like($content->{message}, qr/is missing a message body/, "$self->{name}: check patch missing body response"); } sub check_patch_body_notarray{ @@ -779,7 +800,7 @@ sub check_patch_body_notarray{ { foo => 'bar' }, ); $self->http_code_msg(400, "check patch no array body", $res, $content); - like($content->{message}, qr/must be an array/, "check patch missing body response"); + like($content->{message}, qr/must be an array/, "$self->{name}: check patch missing body response"); } sub check_patch_op_missed{ @@ -788,7 +809,7 @@ sub check_patch_op_missed{ [{ foo => 'bar' }], ); $self->http_code_msg(400, "check patch no op in body", $res, $content); - like($content->{message}, qr/must have an 'op' field/, "check patch no op in body response"); + like($content->{message}, qr/must have an 'op' field/, "$self->{name}: check patch no op in body response"); } sub check_patch_op_wrong{ @@ -797,7 +818,7 @@ sub check_patch_op_wrong{ [{ op => 'bar' }], ); $self->http_code_msg(400, "check patch invalid op in body", $res, $content); - like($content->{message}, qr/Invalid PATCH op /, "check patch no op in body response"); + like($content->{message}, qr/Invalid PATCH op /, "$self->{name}: check patch no op in body response"); } sub check_patch_opreplace_paramsmiss{ @@ -806,7 +827,7 @@ sub check_patch_opreplace_paramsmiss{ [{ op => 'replace' }], ); $self->http_code_msg(400, "check patch missing fields for op", $res, $content); - like($content->{message}, qr/Missing PATCH keys /, "check patch missing fields for op response"); + like($content->{message}, qr/Missing PATCH keys /, "$self->{name}: check patch missing fields for op response"); } sub check_patch_opreplace_paramsextra{ @@ -815,7 +836,7 @@ sub check_patch_opreplace_paramsextra{ [{ op => 'replace', path => '/foo', value => 'bar', invalid => 'sna' }], ); $self->http_code_msg(400, "check patch extra fields for op", $res, $content); - like($content->{message}, qr/Invalid PATCH key /, "check patch extra fields for op response"); + like($content->{message}, qr/Invalid PATCH key /, "$self->{name}: check patch extra fields for op response"); } sub check_patch_path_wrong{ my($self) = @_; @@ -842,16 +863,21 @@ sub check_bundle{ my($self) = @_; $self->check_options_collection(); # iterate over collection to check next/prev links and status - my $listed = $self->check_list_collection(); - $self->check_created_listed($listed); + my $listed=[]; + if($self->methods->{collection}->{allowed}->{GET}){ + $listed = $self->check_list_collection(); + $self->check_created_listed($listed); + } # test model item - if(@$listed){ + if(@$listed && !$self->NO_ITEM_MODULE){ $self->check_options_item; - if(exists $self->methods->{'item'}->{allowed}->{'PUT'}){ - $self->check_put_bundle; - } - if(exists $self->methods->{'item'}->{allowed}->{'PATCH'}){ - $self->check_patch_bundle; + if(!$self->IS_EMPTY_COLLECTION){ + if(exists $self->methods->{'item'}->{allowed}->{'PUT'}){ + $self->check_put_bundle; + } + if(exists $self->methods->{'item'}->{allowed}->{'PATCH'}){ + $self->check_patch_bundle; + } } } } @@ -958,7 +984,7 @@ sub check_get2put{ (defined $put_in->{data_cb}) and $put_in->{data_cb}->($put_out->{content_in}); @{$put_out}{qw/response content request/} = $self->request_put( $put_out->{content_in}, $put_in->{uri} ); $self->http_code_msg(200, "check_get2put: check put successful", $put_out->{response}, $put_out->{content} ); - is_deeply($get_out->{content}, $put_out->{content}, "check_get2put: check put if unmodified put returns the same"); + is_deeply($get_out->{content}, $put_out->{content}, "$self->{name}: check_get2put: check put if unmodified put returns the same"); return ($put_out,$get_out); } @@ -1013,7 +1039,7 @@ sub check_post2get{ delete $get_out->{content}->{_links}; my $item_id = delete $get_out->{content}->{id}; - is_deeply($post_out->{data}, $get_out->{content}, "check_post2get: check POSTed '".$self->name."' against fetched"); + is_deeply($post_out->{data}, $get_out->{content}, "$self->{name}: check_post2get: check POSTed '".$self->name."' against fetched"); $get_out->{content}->{id} = $item_id; return ($post_out, $get_out); @@ -1026,12 +1052,11 @@ sub put_and_get{ @{$get_out}{qw/response content request/} = $self->check_item_get($get_in->{uri}); delete $put_get_out->{content_in}->{_links}; delete $put_get_out->{content_in}->{_embedded}; - is_deeply($put_in->{content}, $put_get_out->{content}, "check that '$put_in->{uri}' was updated on put"); + is_deeply($put_in->{content}, $put_get_out->{content}, "$self->{name}: check that '$put_in->{uri}' was updated on put;"); return ($put_out,$put_get_out,$get_out); } ####--------------------------utils - sub hash2params{ my($self,$hash) = @_; return join '&', map {$_.'='.uri_escape($hash->{$_})} keys %{ $hash }; @@ -1060,6 +1085,8 @@ sub uri2location{ sub http_code_msg{ my($self,$code,$message,$res,$content) = @_; my $message_res; + my $name = $self->{name} // ''; + $message //= ''; if ( ($res->code < 300) || ( $code >= 300 ) ) { my $res_message = $res->message // ''; my $content_message = 'HASH' eq ref $content ? $content->{message} // '' : '' ; @@ -1067,9 +1094,9 @@ sub http_code_msg{ } else { $content //= $self->get_response_content($res); if (defined $content && $content && defined $content->{message}) { - $message_res = $message . ' (' . $res->message . ': ' . $content->{message} . ')'; + $message_res = "$name: ".$message . ' (' . $res->message . ': ' . $content->{message} . ')'; } else { - $message_res = $message . ' (' . $res->message . ')'; + $message_res = "$name: ".$message . ' (' . $res->message . ')'; } } $code and is($res->code, $code, $message_res); @@ -1098,4 +1125,26 @@ sub replace_cached_data{ store $restored,$self->data_cache_file; return $restored; } + +sub get_cached_data{ + my($self) = @_; + return (-e $self->data_cache_file) ? retrieve($self->data_cache_file) : {}; +} + +sub replace_cached_data{ + my($self,$data_callback,$restored) = @_; + $restored //= $self->get_cached_data; + $data_callback->($restored); + store $restored,$self->data_cache_file; + return $restored; +} + +sub clear_cache{ + my($self) = @_; + if(-e $self->data_cache_file){ + my $cmd = "rm ".$self->data_cache_file; + `$cmd`; + } +} + 1;