From 5633770f1fbe503518233debd188497752c745b3 Mon Sep 17 00:00:00 2001 From: Rene Krenn Date: Thu, 24 Nov 2016 03:19:59 +0100 Subject: [PATCH] TT#5559 api/events, event test with "missing end-events" cases + api/events resource + removing an autoattendant via /api/callforwards: events OK + removing an autoattendant via /api/cfdestiantions: OK + removing an autoattendant via /api/cfmappings: OK Change-Id: I4c309753b9338582479dba9951f757bb2ecaad7e --- lib/NGCP/Panel/Controller/API/Events.pm | 195 ++++++++ lib/NGCP/Panel/Controller/API/EventsItem.pm | 102 +++++ lib/NGCP/Panel/Form/Event/Admin.pm | 28 ++ lib/NGCP/Panel/Form/Event/Reseller.pm | 50 +++ lib/NGCP/Panel/Role/API/Events.pm | 86 ++++ lib/NGCP/Panel/Utils/DateTime.pm | 9 + lib/NGCP/Panel/Utils/Events.pm | 4 +- t/api-rest/api-events.t | 470 ++++++++++++++++++++ 8 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/Events.pm create mode 100644 lib/NGCP/Panel/Controller/API/EventsItem.pm create mode 100644 lib/NGCP/Panel/Form/Event/Admin.pm create mode 100644 lib/NGCP/Panel/Form/Event/Reseller.pm create mode 100644 lib/NGCP/Panel/Role/API/Events.pm create mode 100644 t/api-rest/api-events.t diff --git a/lib/NGCP/Panel/Controller/API/Events.pm b/lib/NGCP/Panel/Controller/API/Events.pm new file mode 100644 index 0000000000..cd80e14beb --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/Events.pm @@ -0,0 +1,195 @@ +package NGCP::Panel::Controller::API::Events; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; + +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); + +#use NGCP::Panel::Utils::DateTime; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require NGCP::Panel::Role::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +sub allowed_methods{ + return [qw/GET OPTIONS HEAD/]; +} + +sub api_description { + return 'Browse EDRs (event data records).'; +}; + +sub query_params { + return [ + { + param => 'subscriber_id', + description => 'Filter for events of a specific subscriber.', + query => { + first => sub { + my $q = shift; + return { 'subscriber_id' => $q }; + }, + second => sub {}, + }, + }, + { + param => 'reseller_id', + description => 'Filter for events for customers/subscribers of a specific reseller.', + query => { + first => sub { + my $q = shift; + return { 'reseller.id' => $q }; + }, + second => sub {}, + }, + }, + { + param => 'type', + description => 'Filter for events of a specific type.', + query => { + first => sub { + my $q = shift; + { type => { like => $q } }; + }, + second => sub {}, + }, + }, + { + param => 'timestamp_from', + description => 'Filter for events occurred after or at the given time stamp.', + query => { + first => sub { + my $q = shift; + my $dt = NGCP::Panel::Utils::DateTime::from_string($q); + return { 'timestamp' => { '>=' => $dt->epoch } }; + }, + second => sub {}, + }, + }, + { + param => 'timestamp_to', + description => 'Filter for events occurred before or at the given time stamp.', + query => { + first => sub { + my $q = shift; + my $dt = NGCP::Panel::Utils::DateTime::from_string($q); + return { 'timestamp' => { '<=' => $dt->epoch } }; + }, + second => sub {}, + }, + }, + ]; +} + +use parent qw/Catalyst::Controller NGCP::Panel::Role::API::Events/; + +sub resource_name{ + return 'events'; +} +sub dispatch_path{ + return '/api/events/'; +} +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-events'; +} + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 0, + Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods } + }, + action_roles => [qw(+NGCP::Panel::Role::HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); + my (@embedded, @links); + my $form = $self->get_form($c); + for my $item ($items->all) { + my $hal = $self->hal_from_item($c, $item, $form); + $hal->_forcearray(1); + push @embedded,$hal; + my $link = Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->id), + ); + $link->_forcearray(1); + push @links, $link; + } + 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/'); + + push @links, $self->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params); + + my $hal = Data::HAL->new( + embedded => [@embedded], + links => [@links], + ); + $hal->resource({ + total_count => $total_count, + }); + 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); +} + +1; diff --git a/lib/NGCP/Panel/Controller/API/EventsItem.pm b/lib/NGCP/Panel/Controller/API/EventsItem.pm new file mode 100644 index 0000000000..97c70859f4 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/EventsItem.pm @@ -0,0 +1,102 @@ +package NGCP::Panel::Controller::API::EventsItem; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; + +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); + +#use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ValidateJSON qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +require Catalyst::ActionRole::ACL; +require NGCP::Panel::Role::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +sub allowed_methods{ + return [qw/GET OPTIONS HEAD/]; +} + +use parent qw/Catalyst::Controller NGCP::Panel::Role::API::Events/; + +sub resource_name{ + return 'events'; +} +sub dispatch_path{ + return '/api/events/'; +} +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-events'; +} + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 1, + Does => [qw(ACL RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods } + }, + action_roles => [qw(+NGCP::Panel::Role::HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +sub GET :Allow { + my ($self, $c, $id) = @_; + { + last unless $self->valid_id($c, $id); + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, event => $item); + + 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 + s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|; + s/rel=self/rel="item self"/; + $_ + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c, $id) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c, $id) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Patch => 'application/json-patch+json', + )); + $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); +} + +1; diff --git a/lib/NGCP/Panel/Form/Event/Admin.pm b/lib/NGCP/Panel/Form/Event/Admin.pm new file mode 100644 index 0000000000..403fed517d --- /dev/null +++ b/lib/NGCP/Panel/Form/Event/Admin.pm @@ -0,0 +1,28 @@ +package NGCP::Panel::Form::Event::Admin; + +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::Event::Reseller'; + +has_field 'reseller_id' => ( + type => 'PosInteger', + label => 'The subscriber contract\'s reseller.', + required => 1, +); + +has_field 'export_status' => ( + type => 'Select', + label => 'The status of the exporting process.', + options => [ + { label => 'unexported', 'value' => 'unexported' }, + { label => 'ok', 'value' => 'ok' }, + { label => 'failed', 'value' => 'failed' }, + ], +); + +has_field 'exported_at' => ( + type => 'Text', + title => 'The timestamp when the exporting occured.', + required => 0, +); + +1; diff --git a/lib/NGCP/Panel/Form/Event/Reseller.pm b/lib/NGCP/Panel/Form/Event/Reseller.pm new file mode 100644 index 0000000000..af5bde96fc --- /dev/null +++ b/lib/NGCP/Panel/Form/Event/Reseller.pm @@ -0,0 +1,50 @@ +package NGCP::Panel::Form::Event::Reseller; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; + +has_field 'id' => ( + type => 'Hidden' +); + +has_field 'type' => ( + type => 'Text', + label => 'The event type.', + required => 1, +); + +#has_field 'type' => ( +# type => 'Select', +# label => 'The top-up request type.', +# options => [ +# { value => 'cash', label => 'Cash top-up' }, +# { value => 'voucher', label => 'Voucher top-up' }, +# ], +# required => 1, +#); + +has_field 'subscriber_id' => ( + type => 'PosInteger', + label => 'The subscriber the event is related to.', + required => 1, +); + +has_field 'old_status' => ( + type => 'Text', + label => 'Status information before the event, if applicable.', + required => 0, +); + +has_field 'new_status' => ( + type => 'Text', + label => 'Status information after the event, if applicable.', + required => 0, +); + +has_field 'timestamp' => ( + type => '+NGCP::Panel::Field::DateTime', + label => 'The timestamp of the event.', + required => 1, +); + +1; diff --git a/lib/NGCP/Panel/Role/API/Events.pm b/lib/NGCP/Panel/Role/API/Events.pm new file mode 100644 index 0000000000..48fb68608f --- /dev/null +++ b/lib/NGCP/Panel/Role/API/Events.pm @@ -0,0 +1,86 @@ +package NGCP::Panel::Role::API::Events; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; + +use parent 'NGCP::Panel::Role::API'; + + +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use NGCP::Panel::Form::Event::Reseller; +use NGCP::Panel::Form::Event::Admin; +use Data::Dumper; + +sub _item_rs { + my ($self, $c) = @_; + + my $item_rs = $c->model('DB')->resultset('events'); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ + 'reseller_id' => $c->user->reseller_id, + },undef); + } + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + if($c->user->roles eq "admin") { + return NGCP::Panel::Form::Event::Admin->new; + } elsif($c->user->roles eq "reseller") { + return NGCP::Panel::Form::Event::Reseller->new; + } +} + +sub hal_from_item { + my ($self, $c, $item, $form) = @_; + my %resource = $item->get_inflated_columns; + + my $datetime_fmt = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); + $resource{timestamp} = $datetime_fmt->format_datetime($resource{timestamp}) if defined $resource{timestamp}; + + my $hal = Data::HAL->new( + 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 => 'collection', href => sprintf("/api/%s/", $self->resource_name)), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), + (defined $item->subscriber_id ? Data::HAL::Link->new(relation => 'ngcp:subscribers', href => sprintf("/api/subscribers/%d", $item->subscriber_id)) : ()), + (defined $item->reseller_id ? Data::HAL::Link->new(relation => 'ngcp:resellers', href => sprintf("/api/resellers/%d", $item->reseller_id)) : ()), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= $self->get_form($c); + + $self->validate_form( + c => $c, + resource => \%resource, + form => $form, + run => 0, + exceptions => [qw/id subscriber_id reseller_id/], + ); + + $resource{id} = int($item->id); + $hal->resource({%resource}); + return $hal; +} + +sub item_by_id { + my ($self, $c, $id) = @_; + my $item_rs = $self->item_rs($c); + return $item_rs->find($id); +} + +1; diff --git a/lib/NGCP/Panel/Utils/DateTime.pm b/lib/NGCP/Panel/Utils/DateTime.pm index 35f24a3c84..05f9101eb1 100644 --- a/lib/NGCP/Panel/Utils/DateTime.pm +++ b/lib/NGCP/Panel/Utils/DateTime.pm @@ -27,6 +27,15 @@ sub current_local { } } +sub current_local_hires { + + #If the epoch value is a floating-point value, it will be rounded to nearest microsecond. + return DateTime->from_epoch( epoch => Time::HiRes::time, + time_zone => DateTime::TimeZone->new(name => 'local') + ); + +} + sub set_local_tz { my $dt = shift; if (defined $dt && ref $dt eq 'DateTime' && !is_infinite($dt)) { diff --git a/lib/NGCP/Panel/Utils/Events.pm b/lib/NGCP/Panel/Utils/Events.pm index 3b979d8398..a6e7985811 100644 --- a/lib/NGCP/Panel/Utils/Events.pm +++ b/lib/NGCP/Panel/Utils/Events.pm @@ -11,14 +11,14 @@ sub insert { my $old = $params{old}; my $new = $params{new}; - + $schema->resultset('events')->create({ type => $type, subscriber_id => $subscriber->id, reseller_id => $subscriber->contract->contact->reseller_id, old_status => $old // '', new_status => $new // '', - timestamp => NGCP::Panel::Utils::DateTime::current_local->hires_epoch, + timestamp => NGCP::Panel::Utils::DateTime::current_local_hires, export_status => 'unexported', exported_at => undef, }); diff --git a/t/api-rest/api-events.t b/t/api-rest/api-events.t new file mode 100644 index 0000000000..53b7408eb4 --- /dev/null +++ b/t/api-rest/api-events.t @@ -0,0 +1,470 @@ +#use Sipwise::Base; +use Net::Domain qw(hostfqdn); +use LWP::UserAgent; +use JSON qw(); +use Test::More; + +my $is_local_env = 0; + +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); + +#$ua->add_handler("request_send", sub { +# my ($request, $ua, $h) = @_; +# print $request->method . ' ' . $request->uri . "\n" . ($request->content ? $request->content . "\n" : '') unless $request->header('authorization'); +# return undef; +#}); +#$ua->add_handler("response_done", sub { +# my ($response, $ua, $h) = @_; +# print $response->decoded_content . "\n" if $response->code != 401; +# return undef; +#}); + +my $t = time; +my $reseller_id = 1; + +$req = HTTP::Request->new('POST', $uri.'/api/domains/'); +$req->header('Content-Type' => 'application/json'); +$req->content(JSON::to_json({ + domain => 'test' . $t . '.example.org', + reseller_id => $reseller_id, +})); +$res = $ua->request($req); +is($res->code, 201, "create test domain"); +$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); +$res = $ua->request($req); +is($res->code, 200, "fetch created test domain"); +my $domain = JSON::from_json($res->decoded_content); + +$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/'); +$req->header('Content-Type' => 'application/json'); +$req->header('Prefer' => 'return=representation'); +$req->content(JSON::to_json({ + name => "test profile $t", + handle => "testprofile$t", + reseller_id => $reseller_id, +})); +$res = $ua->request($req); +is($res->code, 201, "create test billing profile"); +my $billing_profile_id = $res->header('Location'); +$billing_profile_id =~ s/^.+\/(\d+)$/$1/; + +$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/'); +$req->header('Content-Type' => 'application/json'); +$req->content(JSON::to_json({ + firstname => "cust_contact_first", + lastname => "cust_contact_last", + email => "cust_contact\@custcontact.invalid", + reseller_id => $reseller_id, +})); +$res = $ua->request($req); +is($res->code, 201, "create test customer contact"); +$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); +$res = $ua->request($req); +is($res->code, 200, "fetch test customer contact"); +my $custcontact = JSON::from_json($res->decoded_content); + +my %subscriber_map = (); +my %customer_map = (); + +#goto SKIP; +{ + my $customer = _create_customer( + type => "sipaccount", + ); + my $subscriber = _create_subscriber($customer, + primary_number => { cc => 888, ac => '1'.(scalar keys %subscriber_map), sn => $t }, + ); + + my $call_forwards = set_callforwards($subscriber,{ cfu => { + destinations => [ + { destination => "5678" }, + { destination => "autoattendant", }, + ], + }}); + $call_forwards = set_callforwards($subscriber,{ cfu => { + destinations => [ + { destination => "5678" }, + ], + }}); + _check_event_history("events generated using /api/callforwards: ",$subscriber->{id},"%ivr",[ + { subscriber_id => $subscriber->{id}, type => "start_ivr" }, + { subscriber_id => $subscriber->{id}, type => "end_ivr" }, + ]); +} + +#$t = time; + +SKIP: +{ + + my $customer = _create_customer( + type => "sipaccount", + ); + my $subscriber = _create_subscriber($customer, + primary_number => { cc => 888, ac => '2'.(scalar keys %subscriber_map), sn => $t }, + ); + + my $destinationset_1 = _create_cfdestinationset($subscriber,"dest1_$t",[{ destination => "1234", + timeout => '10', + priority => '1', + simple_destination => undef },{ destination => "autoattendant", + timeout => '10', + priority => '1', + simple_destination => undef } + ]); + my $destinationset_2 = _create_cfdestinationset($subscriber,"dest2_$t",[{ destination => "1234", + timeout => '10', + priority => '1', + simple_destination => undef },{ destination => "autoattendant", + timeout => '10', + priority => '1', + simple_destination => undef } + ]); + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($t); + my $timeset = _create_cftimeset($subscriber,[{ year => $year + 1900, + month => $mon + 1, + mday => $mday, + wday => $wday + 1, + hour => $hour, + minute => $min}]); + my $mappings = _create_cfmapping($subscriber,{ + #cfb => [{ destinationset => $cfdestinationset->{name}, + # timeset => $cftimeset->{name}}], + #cfna => [{ destinationset => $cfdestinationset->{name}, + # timeset => $cftimeset->{name}}], + #cft => [{ destinationset => $cfdestinationset->{name}, + # timeset => $cftimeset->{name}}], + cfb => [], + cfna => [], + cft => [{ destinationset => $destinationset_1->{name}, + timeset => $timeset->{name}}], + cfu => [{ destinationset => $destinationset_2->{name}, + timeset => $timeset->{name}}], + }); + + #1. update destination set: + $destinationset_1 = _update_cfdestinationset($destinationset_1,[{ destination => "1234", + timeout => '10', + priority => '1', + simple_destination => undef }, + ]); + _check_event_history("events generated by updating /api/cfdestinationsets: ",$subscriber->{id},"%ivr",[ + { subscriber_id => $subscriber->{id}, type => "start_ivr" }, + { subscriber_id => $subscriber->{id}, type => "start_ivr" }, + { subscriber_id => $subscriber->{id}, type => "end_ivr" }, + ]); + #2. update cfmappings: + $mappings = _update_cfmapping($subscriber,"cfu",[]); + _check_event_history("events generated by updating /api/cfmappings: ",$subscriber->{id},"%ivr",[ + { subscriber_id => $subscriber->{id}, type => "start_ivr" }, + { subscriber_id => $subscriber->{id}, type => "start_ivr" }, + { subscriber_id => $subscriber->{id}, type => "end_ivr" }, + { subscriber_id => $subscriber->{id}, type => "end_ivr" }, + ]); + +} + +sub _create_cfmapping { + my ($subscriber,$mappings) = @_; + + my $cfmapping_uri = $uri.'/api/cfmappings/'.$subscriber->{id}; + $req = HTTP::Request->new('PUT', $cfmapping_uri); #$customer->{id}); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json($mappings)); + $res = $ua->request($req); + is($res->code, 200, "create test cfmappings"); + $req = HTTP::Request->new('GET', $cfmapping_uri); # . '?page=1&rows=' . (scalar keys %$put_data)); + $res = $ua->request($req); + is($res->code, 200, "fetch test cfmappings"); + return JSON::from_json($res->decoded_content); +} + +sub _update_cfmapping { + my ($subscriber,$cf_type,$mapping) = @_; + my $cfmapping_uri = $uri.'/api/cfmappings/'.$subscriber->{id}; + $req = HTTP::Request->new('PATCH', $cfmapping_uri); + $req->header('Content-Type' => 'application/json-patch+json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json( + [ { op => 'replace', path => '/'.$cf_type, value => $mapping } ] + )); + $res = $ua->request($req); + is($res->code, 200, "update test cfmappings"); + $req = HTTP::Request->new('GET', $cfmapping_uri); + $res = $ua->request($req); + is($res->code, 200, "fetch updated test cfmappings"); + return JSON::from_json($res->decoded_content); +} + +sub _create_cftimeset { + my ($subscriber,$times) = @_; + + $req = HTTP::Request->new('POST', $uri.'/api/cftimesets/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + name => "cf_time_set_".$t, + subscriber_id => $subscriber->{id}, + times => \@times, + })); + $res = $ua->request($req); + is($res->code, 201, "create test cftimeset"); + my $cftimeset_uri = $uri.'/'.$res->header('Location'); + $req = HTTP::Request->new('GET', $cftimeset_uri); + $res = $ua->request($req); + is($res->code, 200, "fetch created test cftimeset"); + return JSON::from_json($res->decoded_content); + +} + +sub _create_cfdestinationset { + my ($subscriber,$name,$destinations) = @_; + + $req = HTTP::Request->new('POST', $uri.'/api/cfdestinationsets/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + name => $name, + subscriber_id => $subscriber->{id}, + destinations => $destinations, + })); + $res = $ua->request($req); + is($res->code, 201, "create test cfdestinationset"); + my $cfdestinationset_uri = $uri.'/'.$res->header('Location'); + $req = HTTP::Request->new('GET', $cfdestinationset_uri); + $res = $ua->request($req); + is($res->code, 200, "fetch created test cfdestinationset"); + return JSON::from_json($res->decoded_content); + +} + +sub _update_cfdestinationset { + my ($destinationset,$destinations) = @_; + my $cfdestinationset_uri = $uri.'/api/cfdestinationsets/'.$destinationset->{id}; + $req = HTTP::Request->new('PATCH', $cfdestinationset_uri); + $req->header('Content-Type' => 'application/json-patch+json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json( + [ { op => 'replace', path => '/destinations', value => $destinations } ] + )); + $res = $ua->request($req); + is($res->code, 200, "update test cfdestinationset"); + $req = HTTP::Request->new('GET', $cfdestinationset_uri); + $res = $ua->request($req); + is($res->code, 200, "fetch updated test cfdestinationset"); + return JSON::from_json($res->decoded_content); + +} + +sub set_callforwards { + my ($subscriber,$call_forwards) = @_; + + my $callforward_uri = $uri.'/api/callforwards/'.$subscriber->{id}; + $req = HTTP::Request->new('PUT', $callforward_uri); #$customer->{id}); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json($call_forwards)); + $res = $ua->request($req); + is($res->code, 200, "set test callforwards"); + $req = HTTP::Request->new('GET', $callforward_uri); # . '?page=1&rows=' . (scalar keys %$put_data)); + $res = $ua->request($req); + is($res->code, 200, "fetch test callforwards"); + return JSON::from_json($res->decoded_content); + +} + +sub _get_subscriber { + + my ($subscriber) = @_; + $req = HTTP::Request->new('GET', $uri.'/api/subscribers/'.$subscriber->{id}); + $res = $ua->request($req); + is($res->code, 200, "fetch test subscriber"); + $subscriber = JSON::from_json($res->decoded_content); + $subscriber_map{$subscriber->{id}} = $subscriber; + return $subscriber; + +} + +sub _create_subscriber { + + my ($customer,@further_opts) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/subscribers/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + domain_id => $domain->{id}, + username => 'subscriber_' . (scalar keys %subscriber_map) . '_'.$t, + password => 'subscriber_password', + customer_id => $customer->{id}, + #status => "active", + @further_opts, + })); + $res = $ua->request($req); + is($res->code, 201, "create test subscriber"); + $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); + $res = $ua->request($req); + is($res->code, 200, "fetch test subscriber"); + my $subscriber = JSON::from_json($res->decoded_content); + $subscriber_map{$subscriber->{id}} = $subscriber; + return $subscriber; + +} + +sub _update_subscriber { + + my ($subscriber,@further_opts) = @_; + $req = HTTP::Request->new('PUT', $uri.'/api/subscribers/'.$subscriber->{id}); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json({ + %$subscriber, + @further_opts, + })); + $res = $ua->request($req); + is($res->code, 200, "update test subscriber"); + $subscriber = JSON::from_json($res->decoded_content); + $subscriber_map{$subscriber->{id}} = $subscriber; + return $subscriber; + +} + +sub _create_customer { + + my (@further_opts) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/customers/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + status => "active", + contact_id => $custcontact->{id}, + type => "sipaccount", + billing_profile_id => $billing_profile_id, + max_subscribers => undef, + external_id => undef, + #status => "active", + @further_opts, + })); + $res = $ua->request($req); + is($res->code, 201, "create test customer"); + $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); + $res = $ua->request($req); + is($res->code, 200, "fetch test customer"); + my $customer = JSON::from_json($res->decoded_content); + $customer_map{$customer->{id}} = $customer; + return $customer; + +} + + +sub _check_event_history { + + my ($label,$subscriber_id,$type,$expected_events) = @_; + if (defined $subscriber_id) { + $subscriber_id = '&subscriber_id=' . $subscriber_id; + } else { + $subscriber_id = ''; + } + if (defined $type) { + $type = '&type=' . $type; + } else { + $type = ''; + } + + my $total_count = (scalar @$expected_events); + my $i = 0; + my $ok = 1; + my @events = (); + my @requests = (); + my $last_request; + $last_request = _req_to_debug($req) if $req; + my $nexturi = $uri.'/api/events/?page=1&rows=10&order_by_direction=asc&order_by=id'.$subscriber_id.$type; + do { + $req = HTTP::Request->new('GET',$nexturi); + $res = $ua->request($req); + is($res->code, 200, $label . "fetch events collection page"); + push(@requests,_req_to_debug($req)); + my $collection = JSON::from_json($res->decoded_content); + my $selfuri = $uri . $collection->{_links}->{self}->{href}; + my $colluri = URI->new($selfuri); + + $ok = ok($collection->{total_count} == $total_count, $label . "check 'total_count' of collection") && $ok; + + if($collection->{_links}->{next}->{href}) { + $nexturi = $uri . $collection->{_links}->{next}->{href}; + } else { + $nexturi = undef; + } + + $collection->{_embedded}->{'ngcp:events'} = [ + $collection->{_embedded}->{'ngcp:events'} + ] if "HASH" eq ref $collection->{_embedded}->{'ngcp:events'}; + + my $page_items = {}; + + foreach my $event (@{ $collection->{_embedded}->{'ngcp:events'} }) { + $ok = _compare_event($event,$expected_events->[$i],$label) && $ok; + delete $event->{'_links'}; + push(@events,$event); + $i++ + } + + } while($nexturi); + + ok($i == $total_count,$label . "check if all expected items are listed"); + diag(Dumper({last_request => $last_request, collection_requests => \@requests, result => \@events})) if !$ok; + +} + +sub _compare_event { + + my ($got,$expected,$label) = @_; + + my $ok = 1; + + if ($expected->{id}) { + $ok = is($got->{id},$expected->{id},$label . "check event " . $got->{id} . " id") && $ok; + } + + if ($expected->{subscriber_id}) { + $ok = is($got->{subscriber_id},$expected->{subscriber_id},$label . "check event " . $got->{id} . " subscriber_id") && $ok; + } + + if ($expected->{type}) { + $ok = is($got->{type},$expected->{type},$label . "check event " . $got->{id} . " type '".$expected->{type}."'") && $ok; + } + + return $ok; + +} + +sub _req_to_debug { + my $request = shift; + return { request => $request->method . " " . $request->uri, + headers => $request->headers }; +} + +sub _get_query_string { + my ($filters) = @_; + my $query = ''; + foreach my $param (keys %$filters) { + if (length($query) == 0) { + $query .= '?'; + } else { + $query .= '&'; + } + $query .= $param . '=' . $filters->{$param}; + } + return $query; +}; + +done_testing;