diff --git a/lib/NGCP/Panel/Controller/API/CallForwards.pm b/lib/NGCP/Panel/Controller/API/CallForwards.pm index 0eb274cbb3..8d04171a43 100644 --- a/lib/NGCP/Panel/Controller/API/CallForwards.pm +++ b/lib/NGCP/Panel/Controller/API/CallForwards.pm @@ -85,9 +85,10 @@ sub GET :Allow { my $cfs = $self->item_rs($c, "callforwards"); (my $total_count, $cfs) = $self->paginate_order_collection($c, $cfs); my (@embedded, @links); + my $form = $self->get_form($c); for my $cf ($cfs->all) { try { - push @embedded, $self->hal_from_item($c, $cf, "callforwards"); + push @embedded, $self->hal_from_item($c, $cf, $form); push @links, Data::HAL::Link->new( relation => 'ngcp:'.$self->resource_name, href => sprintf('%s%s', $self->dispatch_path, $cf->id), diff --git a/lib/NGCP/Panel/Controller/API/CallForwardsItem.pm b/lib/NGCP/Panel/Controller/API/CallForwardsItem.pm index 829993e1e6..10c9a220d9 100644 --- a/lib/NGCP/Panel/Controller/API/CallForwardsItem.pm +++ b/lib/NGCP/Panel/Controller/API/CallForwardsItem.pm @@ -60,7 +60,7 @@ sub GET :Allow { my $item = $self->item_by_id($c, $id); last unless $self->resource_exists($c, subscriber => $item); - my $hal = $self->hal_from_item($c, $item, "callforwards"); + my $hal = $self->hal_from_item($c, $item); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( (map { # XXX Data::HAL must be able to generate links with multiple relations @@ -111,7 +111,7 @@ sub PATCH :Allow { my $item = $self->item_by_id($c, $id); last unless $self->resource_exists($c, subs_for_callforwards => $item); - my $old_resource = $self->hal_from_item($c, $item, "callforwards")->resource; + my $old_resource = $self->hal_from_item($c, $item)->resource; my $resource = $self->apply_patch($c, $old_resource, $json); last unless $resource; @@ -119,7 +119,7 @@ sub PATCH :Allow { $item = $self->update_item($c, $item, $old_resource, $resource, $form); last unless $item; - my $hal = $self->hal_from_item($c, $item, "callforwards"); + my $hal = $self->hal_from_item($c, $item); last unless $self->add_update_journal_item_hal($c,{ hal => $hal, id => $id }); $guard->commit; @@ -129,7 +129,7 @@ sub PATCH :Allow { $c->response->header(Preference_Applied => 'return=minimal'); $c->response->body(q()); } else { - #my $hal = $self->hal_from_item($c, $callforward, "callforwards"); + #my $hal = $self->hal_from_item($c, $callforward); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( $hal->http_headers, ), $hal->as_json); @@ -162,7 +162,7 @@ sub PUT :Allow { $item = $self->update_item($c, $item, $old_resource, $resource, $form); last unless $item; - my $hal = $self->hal_from_item($c, $item, "callforwards"); + my $hal = $self->hal_from_item($c, $item); last unless $self->add_update_journal_item_hal($c,{ hal => $hal, id => $id }); $guard->commit; @@ -172,7 +172,7 @@ sub PUT :Allow { $c->response->header(Preference_Applied => 'return=minimal'); $c->response->body(q()); } else { - #my $hal = $self->hal_from_item($c, $callforward, "callforwards"); + #my $hal = $self->hal_from_item($c, $callforward); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( $hal->http_headers, ), $hal->as_json); @@ -202,7 +202,7 @@ sub DELETE :Allow { last unless $self->add_delete_journal_item_hal($c,{ hal_from_item => sub { my $self = shift; my ($c) = @_; - return $self->hal_from_item($c,$item,"callforwards"); }, + return $self->hal_from_item($c,$item); }, id => $id}); try { diff --git a/lib/NGCP/Panel/Role/API/CallForwards.pm b/lib/NGCP/Panel/Role/API/CallForwards.pm index bb89611760..c5d08cccf4 100644 --- a/lib/NGCP/Panel/Role/API/CallForwards.pm +++ b/lib/NGCP/Panel/Role/API/CallForwards.pm @@ -23,15 +23,12 @@ sub get_form { } sub hal_from_item { - my ($self, $c, $item, $type) = @_; - my $form; - #my $rwr_form = $self->get_form($c, "rules"); #rules? + my ($self, $c, $item, $form) = @_; + my $type = "callforwards"; my $prov_subs = $item->provisioning_voip_subscriber; - die "no provisioning_voip_subscriber" unless $prov_subs; - my %resource = (subscriber_id => $prov_subs->id); my $hal = Data::HAL->new( @@ -51,16 +48,9 @@ sub hal_from_item { ], relation => 'ngcp:'.$self->resource_name, ); - for my $cf_type (qw/cfu cfb cft cfna/) { - my $mapping = $c->model('DB')->resultset('voip_cf_mappings')->search({ - subscriber_id => $prov_subs->id, - type => $cf_type, - })->first; - if ($mapping) { - $resource{$cf_type} = $self->_contents_from_cfm($c, $mapping, $item); - } else { - $resource{$cf_type} = {}; - } + @resource{qw/cfu cfb cft cfna/} = ({}) x 4; + for my $item_cf ($item->provisioning_voip_subscriber->voip_cf_mappings->all){ + $resource{$item_cf->type} = $self->_contents_from_cfm($c, $item_cf, $item); } if(keys %{$resource{cft}}){ my $ringtimeout_preference = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( @@ -70,6 +60,7 @@ sub hal_from_item { } $form //= $self->get_form($c); + $form->clear(); return unless $self->validate_form( c => $c, form => $form, @@ -82,13 +73,13 @@ sub hal_from_item { } sub item_rs { - my ($self, $c, $type) = @_; + my ($self, $c) = @_; my $item_rs; $item_rs = $c->model('DB')->resultset('voip_subscribers') ->search( { 'me.status' => { '!=' => 'terminated' } }, - { prefetch => 'provisioning_voip_subscriber',}, + { 'prefetch' => { 'provisioning_voip_subscriber' => 'voip_cf_mappings' },}, ); if($c->user->roles eq "reseller") { $item_rs = $item_rs->search({ @@ -102,10 +93,8 @@ sub item_rs { } sub item_by_id { - my ($self, $c, $id, $type) = @_; - - my $item_rs = $self->item_rs($c, $type); - return $self->item_rs($c, $type)->search_rs({'me.id' => $id})->first; + my ($self, $c, $id) = @_; + return $self->item_rs($c)->search_rs({'me.id' => $id})->first; } sub update_item { diff --git a/sandbox/callforwards.pl b/sandbox/callforwards.pl new file mode 100644 index 0000000000..68cb26c22c --- /dev/null +++ b/sandbox/callforwards.pl @@ -0,0 +1,250 @@ +#!/usr/bin/perl + +use strict; + +use Data::Dumper; +use NGCP::Schema; +use NGCP::Panel::Utils::Subscriber; +use NGCP::Panel::Utils::Preferences; +use Test::More; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use Safe::Isa qw($_isa); +use NGCP::Panel::Form::CFSimpleAPI; + +my $schema = NGCP::Schema->connect(); +my $ql_exists = 0; +my ($ql,$ana); + +if($ql_exists){ + #use DBIx::Class::QueryLog; + #use DBIx::Class::QueryLog::Analyzer; + my $ql = DBIx::Class::QueryLog->new; + $schema->storage->debugobj($ql); + $schema->storage->debug(1); + my $ana = DBIx::Class::QueryLog::Analyzer->new({ querylog => $ql }); + $ql->bucket('origin'); +} + +my $time = time(); +print "start;\n"; +#print Dumper($schema); + +my $item_rs = $schema->resultset('voip_subscribers')->search( { + 'me.status' => { '!=' => 'terminated' } + },{ + prefetch => { 'provisioning_voip_subscriber'=>'voip_cf_mappings'}, + #prefetch => 'provisioning_voip_subscriber', + rows => 200, + }, +); +my (@arr_orig,@arr_opt); +for my $item ($item_rs->all) { +# print Dumper({$item->get_inflated_columns}); + my %resource = (); + my $prov_subs = $item->provisioning_voip_subscriber; + for my $cf_type (qw/cfu cfb cft cfna/) { + my $mapping = $schema->resultset('voip_cf_mappings')->search({ + subscriber_id => $prov_subs->id, + type => $cf_type, + })->first; + if ($mapping) { + $resource{$cf_type} = _contents_from_cfm($mapping, $item); + } else { + $resource{$cf_type} = {}; + } + } + if(keys %{$resource{cft}}){ + my $ringtimeout_preference = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( + c => undef, attribute => 'ringtimeout', prov_subscriber => $prov_subs, schema => $schema )->first; + $ringtimeout_preference = $ringtimeout_preference ? $ringtimeout_preference->value : undef; + $resource{cft}{ringtimeout} = $ringtimeout_preference; + } + additional_processing($item, \%resource); + push @arr_orig, \%resource; +} +print "1.time=".(time()-$time).";\n"; +#exit; +if($ql_exists){ + $ql->bucket('optimized'); +} +$time = time(); + +for my $item ($item_rs->all) { + my %resource = (); + my $prov_subs = $item->provisioning_voip_subscriber; + @resource{qw/cfu cfb cft cfna/} = ({}) x 4; + for my $item_cf ($item->provisioning_voip_subscriber->voip_cf_mappings->all){ + $resource{$item_cf->type} = _contents_from_cfm($item_cf, $item); + } + if(keys %{$resource{cft}}){ + my $ringtimeout_preference = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( + c => undef, attribute => 'ringtimeout', prov_subscriber => $prov_subs, schema => $schema )->first; + $ringtimeout_preference = $ringtimeout_preference ? $ringtimeout_preference->value : undef; + $resource{cft}{ringtimeout} = $ringtimeout_preference; + } + additional_processing($item, \%resource); + push @arr_opt, \%resource; +} +print "2.time=".(time()-$time).";\n"; + +is_deeply(\@arr_orig, \@arr_opt, "check that arrays are equiv"); +#print Dumper[\@arr_orig, \@arr_opt]; +if($ql_exists){ + print Dumper $ana->get_totaled_queries_by_bucket; +} + + + + + + +sub additional_processing{ + my($item,$resource) = @_; + my $type=''; + my %resource=%$resource; + my $hal = Data::HAL->new( + links => [ + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => 1, + ), + Data::HAL::Link->new(relation => 'collection', href => sprintf("%s", '')), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf("%s%s", '', $item->id)), + Data::HAL::Link->new(relation => "ngcp:$type", href => sprintf("/api/%s/%s", $type, $item->id)), + Data::HAL::Link->new(relation => 'ngcp:subscribers', href => sprintf("/api/subscribers/%d", $item->id)), + ], + relation => 'ngcp:callforwards', + ); + my $form=NGCP::Panel::Form::CFSimpleAPI->new(); + + validate_form( + form => $form, + resource => \%resource, + run => 0, + ); + + $hal->resource(\%resource); + +} +sub _contents_from_cfm { + my ($cfm_item, $sub) = @_; + my (@times, @destinations); + my $timeset_item = $cfm_item->time_set; + my $dset_item = $cfm_item->destination_set; + for my $time ($timeset_item ? $timeset_item->voip_cf_periods->all : () ) { + push @times, {$time->get_inflated_columns}; + delete @{$times[-1]}{'time_set_id', 'id'}; + } + for my $dest ($dset_item ? $dset_item->voip_cf_destinations->all : () ) { + my ($d, $duri) = NGCP::Panel::Utils::Subscriber::destination_to_field($dest->destination); + my $deflated; + if($d eq "uri") { + $deflated = NGCP::Panel::Utils::Subscriber::uri_deflate($duri,$sub) if $d eq "uri"; + $d = $dest->destination; + } + push @destinations, {$dest->get_inflated_columns, + destination => $d, + $deflated ? (simple_destination => $deflated) : (), + }; + delete @{$destinations[-1]}{'destination_set_id', 'id'}; + } + return {times => \@times, destinations => \@destinations}; +} +sub validate_form { + my (%params) = @_; + + my $resource = $params{resource}; + my $form = $params{form}; + my $run = $params{run} // 1; + my $exceptions = $params{exceptions} // []; + my $form_params = $params{form_params} // {}; + push @{ $exceptions }, "external_id"; + + my @normalized = (); + + # move {xxx_id} into {xxx}{id} for FormHandler + foreach my $key(keys %{ $resource } ) { + my $skip_normalize = grep {/^$key$/} @{ $exceptions }; + if($key =~ /^(.+)_id$/ && !$skip_normalize && !exists $resource->{$1}) { + push @normalized, $1; + $resource->{$1}{id} = delete $resource->{$key}; + } + } + + # remove unknown keys + my %fields = map { $_->name => $_ } $form->fields; + validate_fields($resource, \%fields, $run); + + if($run) { + # check keys/vals + $form->process(params => $resource, posted => 1, %{$form_params} ); + unless($form->validated) { + my $e = join '; ', map { + sprintf 'field=\'%s\', input=\'%s\', errors=\'%s\'', + ($_->parent->$_isa('HTML::FormHandler::Field') ? $_->parent->name . '_' : '') . $_->name, + $_->input // '', + join('', @{ $_->errors }) + } $form->error_fields; + return; + } + } + + # move {xxx}{id} back into {xxx_id} for DB + foreach my $key(@normalized) { + next unless(exists $resource->{$key}); + $resource->{$key . '_id'} = defined($resource->{$key}{id}) ? + int($resource->{$key}{id}) : + $resource->{$key}{id}; + delete $resource->{$key}; + } + + return 1; +} + +sub validate_fields { + my ($resource, $fields, $run) = @_; + + for my $k (keys %{ $resource }) { + #if($resource->{$k}->$_isa('JSON::XS::Boolean') || $resource->{$k}->$_isa('JSON::PP::Boolean')) { + if($resource->{$k}->$_isa('JSON::PP::Boolean')) { + $resource->{$k} = $resource->{$k} ? 1 : 0; + } + unless(exists $fields->{$k}) { + delete $resource->{$k}; + } + $resource->{$k} = DateTime::Format::RFC3339->format_datetime($resource->{$k}) + if $resource->{$k}->$_isa('DateTime'); + $resource->{$k} = $resource->{$k} + 0 + if(defined $resource->{$k} && ( + $fields->{$k}->$_isa('HTML::FormHandler::Field::Integer') || + $fields->{$k}->$_isa('HTML::FormHandler::Field::Money') || + $fields->{$k}->$_isa('HTML::FormHandler::Field::Float')) && + (is_int($resource->{$k}) || is_decimal($resource->{$k}))); + + if (defined $resource->{$k} && + $fields->{$k}->$_isa('HTML::FormHandler::Field::Repeatable') && + "ARRAY" eq ref $resource->{$k} ) { + for my $elem (@{ $resource->{$k} }) { + my ($subfield_instance) = $fields->{$k}->fields; + my %subfields = map { $_->name => $_ } $subfield_instance->fields; + validate_fields($elem, \%subfields, $run); + } + } + + # only do this for converting back from obj to hal + # otherwise it breaks db fields with the \0 and \1 notation + unless($run) { + $resource->{$k} = $resource->{$k} ? JSON::true : JSON::false + if(defined $resource->{$k} && + $fields->{$k}->$_isa('HTML::FormHandler::Field::Boolean')); + } + } + + return 1; +} + +1; \ No newline at end of file diff --git a/sandbox/callforwards_form_starter.pl b/sandbox/callforwards_form_starter.pl new file mode 100644 index 0000000000..6799673746 --- /dev/null +++ b/sandbox/callforwards_form_starter.pl @@ -0,0 +1,12 @@ +#!/usr/bin/perl + +use strict; + +use Data::Dumper; + +use NGCP::Panel::Form::CFSimpleAPI; + +for(my $i=0; $i<200;$i++){ + my $form=NGCP::Panel::Form::CFSimpleAPI->new(); +} +1; \ No newline at end of file diff --git a/t/api-rest/api-billingfees.t b/t/api-rest/api-billingfees.t index 6492d9486b..d24c8aede4 100644 --- a/t/api-rest/api-billingfees.t +++ b/t/api-rest/api-billingfees.t @@ -29,7 +29,7 @@ $fake_data->set_data_from_script({ }); my $test_machine = Test::Collection->new( name => 'billingfees', - embedded => [qw/billingzones billingprofiles/] + embedded_resources => [qw/billingzones billingprofiles/] ); $test_machine->DATA_ITEM_STORE($fake_data->process('billingfees')); $test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; @@ -115,7 +115,7 @@ $test_machine->check_bundle(); } { my($res,$item_put,$req) = $test_machine->check_get2put(); - $test_machine->check_embedded($item_put); + $test_machine->check_embedded($item_put->{content}); } { my($res,$mod_fee) = $test_machine->check_patch_correct( [ { op => 'replace', path => '/direction', value => 'in' } ] ); diff --git a/t/api-rest/api-callforwards.t b/t/api-rest/api-callforwards.t index fa752ae95e..895f7359af 100644 --- a/t/api-rest/api-callforwards.t +++ b/t/api-rest/api-callforwards.t @@ -1,281 +1,115 @@ -use Sipwise::Base; -use Net::Domain qw(hostfqdn); -use LWP::UserAgent; -use JSON qw(); -use Test::More; - -my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443'); -my ($netloc) = ($uri =~ m!^https?://(.*)/?.*$!); - -my ($ua, $req, $res); -$ua = LWP::UserAgent->new; - -$ua->ssl_opts( - verify_hostname => 0, - SSL_verify_mode => 0, - ); -my $user = $ENV{API_USER} // 'administrator'; -my $pass = $ENV{API_PASS} // 'administrator'; -$ua->credentials($netloc, "api_admin_http", $user, $pass); - -# OPTIONS tests -{ - $req = HTTP::Request->new('OPTIONS', $uri.'/api/callforwards/'); - $res = $ua->request($req); - is($res->code, 200, "check options request"); - is($res->header('Accept-Post'), "application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-callforwards", "check Accept-Post header in options response"); - my $opts = JSON::from_json($res->decoded_content); - my @hopts = split /\s*,\s*/, $res->header('Allow'); - ok(exists $opts->{methods} && ref $opts->{methods} eq "ARRAY", "check for valid 'methods' in body"); - foreach my $opt(qw( GET HEAD OPTIONS )) { - ok(grep(/^$opt$/, @hopts), "check for existence of '$opt' in Allow header"); - ok(grep(/^$opt$/, @{ $opts->{methods} }), "check for existence of '$opt' in body"); - } -} - -my $t = time; -my $reseller_id = 1; -my $billing_profile_id; #dummy - -# collection test -my $firstcf = undef; -my $firstcustomer; #dummy -my $custcontact = undef; #dummy -my @allcustomers = (); #dummy -my $system_contact_id; #dummy -{ - # iterate over customers collection to check next/prev links and status - my $nexturi = $uri.'/api/callforwards/?page=1&rows=5'; - do { - $res = $ua->get($nexturi); - is($res->code, 200, "fetch cfs page"); - my $collection = JSON::from_json($res->decoded_content); - my $selfuri = $uri . $collection->{_links}->{self}->{href}; - is($selfuri, $nexturi, "check _links.self.href of collection"); - my $colluri = URI->new($selfuri); - - cmp_ok($collection->{total_count}, '>', 0, "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'"); - my $page = int($q{page}); - my $rows = int($q{rows}); - if($page == 1) { - ok(!exists $collection->{_links}->{prev}->{href}, "check absence of 'prev' on first page"); - } else { - ok(exists $collection->{_links}->{prev}->{href}, "check existence of 'prev'"); - } - if(($collection->{total_count} / $rows) <= $page) { - ok(!exists $collection->{_links}->{next}->{href}, "check absence of 'next' on last page"); - } else { - ok(exists $collection->{_links}->{next}->{href}, "check existence of 'next'"); - } +use strict; +use warnings; - if($collection->{_links}->{next}->{href}) { - $nexturi = $uri . $collection->{_links}->{next}->{href}; - } else { - $nexturi = undef; - } - - # TODO: I'd expect that to be an array ref in any case! - ok((ref $collection->{_links}->{'ngcp:callforwards'} eq "ARRAY" || - ref $collection->{_links}->{'ngcp:callforwards'} eq "HASH"), "check if 'ngcp:callforwards' is array/hash-ref"); - - # remove any contact we find in the collection for later check - if(ref $collection->{_links}->{'ngcp:callforwards'} eq "HASH") { - ok(exists $collection->{_embedded}->{'ngcp:callforwards'}->{_links}->{'ngcp:callforwards'}, "check presence of ngcp:callforwards relation"); - ok(exists $collection->{_embedded}->{'ngcp:callforwards'}->{_links}->{'ngcp:subscribers'}, "check presence of ngcp:subscribers relation"); - } else { - foreach my $c(@{ $collection->{_embedded}->{'ngcp:callforwards'} }) { - ok(exists $c->{_links}->{'ngcp:callforwards'}, "check presence of ngcp:callforwards relation"); - ok(exists $c->{_links}->{'ngcp:subscribers'}, "check presence of ngcp:subscribers relation"); +use Test::More; +use Test::Collection; +use Test::FakeData; +use Data::Dumper; + + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + 'callforwards' => { + 'data' => { + #not really necessary - there isn't POST method + #subscriber_id => sub { return shift->get_id('subscribers',@_); }, + cfu => { + destinations => [ + { destination => "12345", timeout => 200}, + ], + times => undef, + }, + cft => { + destinations => [ + { destination => "5678" }, + { destination => "voicebox", timeout => 500 }, + ], + ringtimeout => 10, } - } - - } while($nexturi); -} - - -diag('Note that the next tests require at least one subscriber to be present'); + }, + }, +}); + +my $test_machine = Test::Collection->new( + name => 'callforwards', + embedded_resources => [qw/subscribers callforwards/], +); +$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS)}; +$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; +$test_machine->DATA_ITEM_STORE($fake_data->process('callforwards')); +$test_machine->form_data_item( ); + +SKIP:{ + my ($res,$req,$content); + my $cf1 = $test_machine->get_item_hal(); + + if(!$cf1->{content}->{total_count}){ + skip("Testing requires at least one present callforward. No creation is available.",1); + } -# fetch a callforward (subscriber) id for later tests -$req = HTTP::Request->new('GET', $uri.'/api/callforwards/?page=1&rows=1'); -$res = $ua->request($req); -is($res->code, 200, "fetch first callforward"); -my $cf1 = JSON::from_json($res->decoded_content); -my ($cf1_id) = $cf1->{_embedded}->{'ngcp:callforwards'}->{_links}{self}{href} =~ m!callforwards/([0-9]*)$!; + $test_machine->check_bundle(); -cmp_ok ($cf1_id, '>', 0, "should be positive integer"); + my($cf1_id) = $test_machine->get_id_from_hal($cf1->{content}); #($cf1,'callforwards'); + cmp_ok ($cf1_id, '>', 0, "should be positive integer"); + my $cf1single_uri = "/api/callforwards/$cf1_id"; + my $cf1single; + (undef, $cf1single) = $test_machine->check_item_get($cf1single_uri,"fetch cf id $cf1_id"); -# test cf item -{ - $req = HTTP::Request->new('OPTIONS', "$uri/api/callforwards/$cf1_id"); - $res = $ua->request($req); - is($res->code, 200, "check options on item"); - my @hopts = split /\s*,\s*/, $res->header('Allow'); - my $opts = JSON::from_json($res->decoded_content); - ok(exists $opts->{methods} && ref $opts->{methods} eq "ARRAY", "check for valid 'methods' in body"); - foreach my $opt(qw( GET HEAD OPTIONS PUT PATCH DELETE )) { - ok(grep(/^$opt$/, @hopts), "check for existence of '$opt' in Allow header"); - ok(grep(/^$opt$/, @{ $opts->{methods} }), "check for existence of '$opt' in body"); - } - foreach my $opt(qw( POST )) { - ok(!grep(/^$opt$/, @hopts), "check for absence of '$opt' in Allow header"); - ok(!grep(/^$opt$/, @{ $opts->{methods} }), "check for absence of '$opt' in body"); - } - - # get our cf - $req = HTTP::Request->new('GET', "$uri/api/callforwards/$cf1_id"); - - $res = $ua->request($req); - is($res->code, 200, "fetch cf id $cf1_id"); - my $cf1single = JSON::from_json($res->decoded_content); + #check cf structure is(ref $cf1single, "HASH", "cf should be hash"); ok(exists $cf1single->{cfu}, "cf should have key cfu"); ok(exists $cf1single->{cfb}, "cf should have key cfb"); ok(exists $cf1single->{cft}, "cf should have key cft"); ok(exists $cf1single->{cfna}, "cf should have key cfna"); - # write this cf - $req = HTTP::Request->new('PUT', "$uri/api/callforwards/$cf1_id"); - $req->header('Prefer' => "return=representation"); - $req->header('Content-Type' => 'application/json'); - $req->content(JSON::to_json({ - cfu => { - destinations => [ - { destination => "12345", timeout => 200}, - ], - times => undef, - }, - cft => { - destinations => [ - { destination => "5678" }, - { destination => "voicebox", timeout => 500 }, - ], - ringtimeout => 10, - } - })); - $res = $ua->request($req); - is($res->code, 200, "write a specific callforward") || diag ($res->message); - my $cf1put = JSON::from_json($res->decoded_content); - is (ref $cf1put, "HASH", "should be hashref"); - is ($cf1put->{cfu}{destinations}->[0]->{timeout}, 200, "Check timeout of cft"); - is ($cf1put->{cft}{destinations}->[0]->{simple_destination}, "5678", "Check first destination of cft"); - like ($cf1put->{cft}{destinations}->[0]->{destination}, qr/^sip:5678@/, "Check first destination of cft (regex, full uri)"); - is ($cf1put->{cft}{destinations}->[1]->{destination}, "voicebox", "Check second destination of cft"); + #write cf and check written values + my($cf1_put,$cf1_get) = $test_machine->check_put2get({data_in => $test_machine->DATA_ITEM, uri => $cf1single_uri},undef, 1 ); + is (ref $cf1_put->{content}, "HASH", "should be hashref"); + is ($cf1_put->{content}->{cfu}{destinations}->[0]->{timeout}, 200, "Check timeout of cft"); + is ($cf1_put->{content}->{cft}{destinations}->[0]->{simple_destination}, "5678", "Check first destination of cft"); + like ($cf1_put->{content}->{cft}{destinations}->[0]->{destination}, qr/^sip:5678@/, "Check first destination of cft (regex, full uri)"); + is ($cf1_put->{content}->{cft}{destinations}->[1]->{destination}, "voicebox", "Check second destination of cft"); #write invalid 'timeout' - $req = HTTP::Request->new('PUT', "$uri/api/callforwards/$cf1_id"); - $req->header('Prefer' => "return=representation"); - $req->header('Content-Type' => 'application/json'); - $req->content(JSON::to_json({ + ($res,$content,$req) = $test_machine->request_put({ cfu => { destinations => [ { destination => "12345", timeout => "foobar"}, ], times => undef, }, - })); - $res = $ua->request($req); - is($res->code, 422, "create customer with invalid type"); - my $err = JSON::from_json($res->decoded_content); - is($err->{code}, "422", "check error code in body"); - like($err->{message}, qr/Validation failed/, "check error message in body"); + }, $cf1single_uri); + $test_machine->http_code_msg(422, "create callforward with invalid timeout", $res, $content); + is($content->{code}, "422", "check error code in body"); + like($content->{message}, qr/Validation failed/, "check error message in body"); # get invalid cf - $req = HTTP::Request->new('GET', "$uri/api/callforwards/abc"); - $res = $ua->request($req); + ($res, $content) = $test_machine->request_get("/api/callforwards/abc"); is($res->code, 400, "try invalid callforward id"); - $err = JSON::from_json($res->decoded_content); - is($err->{code}, "400", "check error code in body"); - like($err->{message}, qr/Invalid id/, "check error message in body"); - - # PUT same result again - my $old_cf1 = { %$cf1put }; - delete $cf1put->{_links}; - delete $cf1put->{_embedded}; - $req = HTTP::Request->new('PUT', "$uri/api/callforwards/$cf1_id"); - - # check if it fails without content type - $req->remove_header('Content-Type'); - $req->header('Prefer' => "return=minimal"); - $res = $ua->request($req); - is($res->code, 415, "check put missing content type"); - - # check if it fails with unsupported content type - $req->header('Content-Type' => 'application/xxx'); - $res = $ua->request($req); - is($res->code, 415, "check put invalid content type"); - - $req->remove_header('Content-Type'); - $req->header('Content-Type' => 'application/json'); + is($content->{code}, "400", "check error code in body"); + like($content->{message}, qr/Invalid id/, "check error message in body"); - # check if it fails with invalid Prefer - $req->header('Prefer' => "return=invalid"); - $res = $ua->request($req); - is($res->code, 400, "check put invalid prefer"); + my($cf2_put,$cf2_get) = $test_machine->check_put2get({data_in => $cf1_put->{content}, uri => $cf1single_uri},undef, 1 ); + is_deeply($cf1_put->{content}, $cf2_put->{content}, "check put if unmodified put returns the same"); + $test_machine->check_embedded($cf2_put->{content}); - $req->remove_header('Prefer'); - $req->header('Prefer' => "return=representation"); - - # check if it fails with missing body - $res = $ua->request($req); - is($res->code, 400, "check put no body"); - - # check if put is ok - $req->content(JSON::to_json($cf1put)); - $res = $ua->request($req); - is($res->code, 200, "check put successful"); - - my $new_cf1 = JSON::from_json($res->decoded_content); - is_deeply($old_cf1, $new_cf1, "check put if unmodified put returns the same"); - - # check if we have the proper links - ok(exists $new_cf1->{_links}->{'ngcp:callforwards'}, "check put presence of ngcp:customercontacts relation"); - ok(exists $new_cf1->{_links}->{'ngcp:subscribers'}, "check put presence of ngcp:billingprofiles relation"); - - - $req = HTTP::Request->new('PATCH', "$uri/api/callforwards/$cf1_id"); - $req->header('Prefer' => 'return=representation'); - $req->header('Content-Type' => 'application/json-patch+json'); - - $req->content(JSON::to_json( - [ { op => 'replace', path => '/cfu/destinations/0/timeout', value => '123' } ] - )); - $res = $ua->request($req); - is($res->code, 200, "check patched cf item"); - my $mod_cf1 = JSON::from_json($res->decoded_content); + my $mod_cf1; + ($res,$mod_cf1) = $test_machine->check_patch_correct( [ { op => 'replace', path => '/cfu/destinations/0/timeout', value => '123' } ] ); is($mod_cf1->{cfu}{destinations}->[0]->{timeout}, "123", "check patched replace op"); - is($mod_cf1->{_links}->{self}->{href}, "/api/callforwards/$cf1_id", "check patched self link"); - is($mod_cf1->{_links}->{collection}->{href}, '/api/callforwards/', "check patched collection link"); - - $req->content(JSON::to_json( - [ { op => 'add', path => '/cfu/destinations/-', value => {destination => 99999} } ] - )); - $res = $ua->request($req); + ($res,$mod_cf1) = $test_machine->request_patch( [ { op => 'add', path => '/cfu/destinations/-', value => {destination => 99999} } ] ); is($res->code, 200, "check patch, add a cfu destination"); - - $req->content(JSON::to_json( - [ { op => 'replace', path => '/cfu/destinations/0/timeout', value => "" } ] - )); - $res = $ua->request($req); + ($res,$mod_cf1) = $test_machine->request_patch( [ { op => 'replace', path => '/cfu/destinations/0/timeout', value => "" } ] ); is($res->code, 422, "check patched undef timeout"); - $req->content(JSON::to_json( - [ { op => 'replace', path => '/cfu/destinations/0/timeout', value => 'invalid' } ] - )); - $res = $ua->request($req); + ($res,$mod_cf1) = $test_machine->request_patch( [ { op => 'replace', path => '/cfu/destinations/0/timeout', value => 'invalid' } ] ); is($res->code, 422, "check patched invalid status"); - $req->content(JSON::to_json( - [ { op => 'replace', path => '/some/path', value => 'invalid' } ] - )); - $res = $ua->request($req); - is($res->code, 422, "check patched invalid path"); } done_testing; +1; + # vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-faxes.t b/t/api-rest/api-faxes.t index 720fde607b..cc39ec6738 100644 --- a/t/api-rest/api-faxes.t +++ b/t/api-rest/api-faxes.t @@ -31,7 +31,7 @@ $fake_data->set_data_from_script({ }); my $test_machine = Test::Collection->new( name => 'faxes', - embedded => [qw/subscribers/] + embedded_resources => [qw/subscribers/] ); diff --git a/t/api-rest/api-pbxdevicemodels.t b/t/api-rest/api-pbxdevicemodels.t index 49033e4bbf..88ddbf7fef 100644 --- a/t/api-rest/api-pbxdevicemodels.t +++ b/t/api-rest/api-pbxdevicemodels.t @@ -80,7 +80,7 @@ $fake_data->set_data_from_script({ }); my $test_machine = Test::Collection->new( name => 'pbxdevicemodels', - embedded => [qw/pbxdevicefirmwares/] + embedded_resources => [qw/pbxdevicefirmwares/] ); $test_machine->DATA_ITEM_STORE($fake_data->process('pbxdevicemodels')); @{$test_machine->content_type}{qw/POST PUT/} = (('multipart/form-data') x 2); diff --git a/t/api-rest/api-pbxdevices.t b/t/api-rest/api-pbxdevices.t index 3ab0fde687..524952767a 100644 --- a/t/api-rest/api-pbxdevices.t +++ b/t/api-rest/api-pbxdevices.t @@ -12,7 +12,7 @@ use Test::FakeData; #init test_machine my $test_machine = Test::Collection->new( name => 'pbxdevices', - embedded => [qw/pbxdeviceprofiles customers/], + embedded_resources => [qw/pbxdeviceprofiles customers/], ); $test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; $test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; diff --git a/t/api-rest/api-trustedsources.t b/t/api-rest/api-trustedsources.t index 2d5a8e7bce..a6811d27b5 100644 --- a/t/api-rest/api-trustedsources.t +++ b/t/api-rest/api-trustedsources.t @@ -9,7 +9,7 @@ use Data::Dumper; #init test_machine my $test_machine = Test::Collection->new( name => 'trustedsources', - embedded => [qw/subscribers/], + embedded_resources => [qw/subscribers/], ); $test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; $test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; diff --git a/t/lib/Test/Collection.pm b/t/lib/Test/Collection.pm index d5c5aefb39..e3650660d1 100644 --- a/t/lib/Test/Collection.pm +++ b/t/lib/Test/Collection.pm @@ -673,7 +673,7 @@ 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->DATA_CREATED->{FIRST}, "check patched self link"); + 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"); return ($res,$rescontent,$req); } @@ -754,6 +754,13 @@ sub check_patch_opreplace_paramsextra{ $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"); } +sub check_patch_path_wrong{ + my($self) = @_; + my ($res,$content,$req) = $self->request_patch( + [ { op => 'replace', path => '/some/path', value => 'invalid' } ], + ); + $self->http_code_msg(422, "check patched invalid path", $res, $content); +} sub check_patch_bundle{ my($self) = @_; @@ -766,6 +773,7 @@ sub check_patch_bundle{ $self->check_patch_op_wrong; $self->check_patch_opreplace_paramsmiss; $self->check_patch_opreplace_paramsextra; + $self->check_patch_path_wrong; } sub check_bundle{ my($self) = @_; @@ -878,7 +886,7 @@ sub check_get2put{ } sub check_put2get{ - my($self, $put_in, $get_in) = @_; + my($self, $put_in, $get_in, $nocheck) = @_; my($put_out,$get_out); @@ -888,7 +896,6 @@ sub check_put2get{ $get_out->{uri} = $get_in->{uri}; $put_out->{content_in} = $self->process_data($put_in->{data_cb}, $put_in->{data_in}); - $put_out->{content_in} = JSON::to_json($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_put2get: check put successful",$put_out->{response}, $put_out->{content}); @@ -896,8 +903,9 @@ sub check_put2get{ delete $get_out->{content}->{_links}; delete $get_out->{content}->{_embedded}; my $item_id = delete $get_out->{content}->{id}; - $put_out->{content_in} = JSON::from_json($put_out->{content_in}); - is_deeply($put_out->{content_in}, $get_out->{content}, "check_put2get: check PUTed item against POSTed item"); + if(!$nocheck){ + is_deeply($put_out->{content_in}, $get_out->{content}, "check_put2get: check PUTed item against POSTed item"); + } $get_out->{content}->{id} = $item_id; return ($put_out,$get_out); } @@ -954,6 +962,17 @@ sub resource_clear_file{ print "cmd=$cmd;\n"; `$cmd`; } +sub get_id_from_hal{ + my($self,$hal,$name) = @_; + $name //= $self->name; + my $id = $hal->{_embedded}->{'ngcp:'.$name}->{_links}{self}{href} =~ m!${name}/([0-9]*)$!; + return $id; +} +sub uri2location{ + my($self,$uri) = @_; + $uri=~s/^.*?(\/api\/.*$)/$1/; + return $uri; +} sub http_code_msg{ my($self,$code,$message,$res,$content) = @_; my $message_res;