diff --git a/lib/NGCP/Panel/Controller/API/RewriteRuleSets.pm b/lib/NGCP/Panel/Controller/API/RewriteRuleSets.pm index 8deaa0f3d5..6ab9070d94 100644 --- a/lib/NGCP/Panel/Controller/API/RewriteRuleSets.pm +++ b/lib/NGCP/Panel/Controller/API/RewriteRuleSets.pm @@ -1,22 +1,21 @@ package NGCP::Panel::Controller::API::RewriteRuleSets; -use NGCP::Panel::Utils::Generic qw(:all); -use Sipwise::Base; +use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::RewriteRuleSets/; -use boolean qw(true); -use NGCP::Panel::Utils::DataHal qw(); -use NGCP::Panel::Utils::DataHalLink qw(); -use HTTP::Headers qw(); +use Sipwise::Base; +use NGCP::Panel::Utils::Generic qw(:all); use HTTP::Status qw(:constants); -use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Rewrite; -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; + +__PACKAGE__->set_config(); + +sub _set_config{ + my ($self, $method) = @_; + return { + own_transaction_control => { POST => 1 }, + }; +} sub allowed_methods{ return [qw/GET POST OPTIONS HEAD/]; @@ -31,240 +30,42 @@ sub query_params { { param => 'reseller_id', description => 'Filter for rewriterulesets belonging to a specific reseller', - query => { - first => sub { - my $q = shift; - { reseller_id => $q }; - }, - second => sub {}, - }, + query_type => 'string_eq', }, { param => 'description', description => 'Filter rulesets for a certain description (wildcards possible).', - query => { - first => sub { - my $q = shift; - return { description => { like => $q } }; - }, - second => sub {}, - }, - }, + query_type => 'string_like', + }, { param => 'name', description => 'Filter rulesets for a certain name (wildcards possible).', - query => { - first => sub { - my $q = shift; - return { name => { like => $q } }; - }, - second => sub {}, - }, + query_type => 'string_like', }, ]; } -use parent qw/Catalyst::Controller NGCP::Panel::Role::API::RewriteRuleSets/; - -sub resource_name{ - return 'rewriterulesets'; -} -sub dispatch_path{ - return '/api/rewriterulesets/'; -} -sub relation{ - return 'http://purl.org/sipwise/ngcp-api/#rel-rewriterulesets'; -} - -__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 }, - }, -); - -sub gather_default_action_roles { - my ($self, %args) = @_; my @roles = (); - push @roles, 'NGCP::Panel::Role::HTTPMethods' if $args{attributes}->{Method}; - return @roles; -} - -sub auto :Private { - my ($self, $c) = @_; - - $self->set_body($c); - $self->log_request($c); - return 1; -} - -sub GET :Allow { - my ($self, $c) = @_; - my $page = $c->request->params->{page} // 1; - my $rows = $c->request->params->{rows} // 10; - { - my $rwr_set = $self->item_rs($c, "rulesets"); - (my $total_count, $rwr_set) = $self->paginate_order_collection($c, $rwr_set); - my (@embedded, @links); - for my $set ($rwr_set->all) { - push @embedded, $self->hal_from_item($c, $set, "rewriterulesets"); - push @links, NGCP::Panel::Utils::DataHalLink->new( - relation => 'ngcp:'.$self->resource_name, - href => sprintf('%s%d', $self->dispatch_path, $set->id), - ); - } - push @links, - NGCP::Panel::Utils::DataHalLink->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - NGCP::Panel::Utils::DataHalLink->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - NGCP::Panel::Utils::DataHalLink->new(relation => 'self', href => sprintf('%s?page=%s&rows=%s', $self->dispatch_path, $page, $rows)); - if(($total_count / $rows) > $page ) { - push @links, NGCP::Panel::Utils::DataHalLink->new(relation => 'next', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page + 1, $rows)); - } - if($page > 1) { - push @links, NGCP::Panel::Utils::DataHalLink->new(relation => 'prev', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page - 1, $rows)); - } - - my $hal = NGCP::Panel::Utils::DataHal->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 POST :Allow { - my ($self, $c) = @_; - - my $guard = $c->model('DB')->txn_scope_guard; +sub create_item { + my ($self, $c, $resource, $form, $process_extras) = @_; + my $schema = $c->model('DB'); + my $item; + my $guard = $schema->txn_scope_guard; { - my $schema = $c->model('DB'); - my $resource = $self->get_valid_post_data( - c => $c, - media_type => 'application/json', - ); - last unless $resource; - - my $reseller_id; - if($c->user->roles eq "admin") { - try { - $reseller_id = $resource->{reseller_id} - || $c->user->contract->contact->reseller_id; - } - } elsif($c->user->roles eq "reseller") { - $reseller_id = $c->user->reseller_id; - } - $resource->{reseller_id} = $reseller_id; - - my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id}); - unless($reseller) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id', doesn't exist."); - last; - } - - my $rewriterules = $resource->{rewriterules}; - - my $form = $self->get_form($c); - last unless $self->validate_form( - c => $c, - resource => $resource, - form => $form, - ); - - my $ruleset_test = $schema->resultset('voip_rewrite_rule_sets')->search_rs({ - name => $resource->{name} - })->first; - if ($ruleset_test) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Ruleset with this 'name' already exists."); - last; - } - - my $ruleset; - + my $rewriterules = delete $resource->{rewriterules}; try { - $ruleset = $schema->resultset('voip_rewrite_rule_sets')->create($resource); + $item = $schema->resultset('voip_rewrite_rule_sets')->create($resource); } catch($e) { $c->log->error("failed to create rewriteruleset: $e"); # TODO: user, message, trace, ... $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create rewriteruleset."); - last; + return; } - if ($rewriterules) { - my $i = 30; - if (ref($rewriterules) ne "ARRAY") { - $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "rewriterules must be an array."); - } - for my $rule (@{ $rewriterules }) { - my $rule_form = $self->get_form($c, "rules"); - last unless $self->validate_form( - c => $c, - resource => $rule, - form => $rule_form, - ); - try { - $ruleset->voip_rewrite_rules->create({ - %{ $rule }, - priority => $i++, - }); - } catch($e) { - $c->log->error("failed to create rewriterules: $e"); # TODO: user, message, trace, ... - $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create rewrite rules."); - last; - } - } + $self->update_rewriterules( $c, $item, $rewriterules ); } - $guard->commit; NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); - - $c->response->status(HTTP_CREATED); - $c->response->header(Location => sprintf('/%s%d', $c->request->path, $ruleset->id)); - $c->response->body(q()); } - return; -} - -sub end : Private { - my ($self, $c) = @_; - - $self->log_response($c); - return 1; + return $item; } 1; diff --git a/lib/NGCP/Panel/Controller/API/RewriteRuleSetsItem.pm b/lib/NGCP/Panel/Controller/API/RewriteRuleSetsItem.pm index 6bdf96e58f..7904a118d1 100644 --- a/lib/NGCP/Panel/Controller/API/RewriteRuleSetsItem.pm +++ b/lib/NGCP/Panel/Controller/API/RewriteRuleSetsItem.pm @@ -1,223 +1,68 @@ package NGCP::Panel::Controller::API::RewriteRuleSetsItem; -use NGCP::Panel::Utils::Generic qw(:all); -use Sipwise::Base; +use parent qw/NGCP::Panel::Role::EntitiesItem NGCP::Panel::Role::API::RewriteRuleSets/; -use boolean qw(true); -use NGCP::Panel::Utils::DataHal qw(); -use NGCP::Panel::Utils::DataHalLink qw(); -use HTTP::Headers qw(); +use Sipwise::Base; +use NGCP::Panel::Utils::Generic qw(:all); use HTTP::Status qw(:constants); -use NGCP::Panel::Utils::ValidateJSON qw(); -use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Rewrite; -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 PATCH PUT DELETE/]; -} -use parent qw/Catalyst::Controller NGCP::Panel::Role::API::RewriteRuleSets/; +__PACKAGE__->set_config(); -sub resource_name{ - return 'rewriterulesets'; -} -sub dispatch_path{ - return '/api/rewriterulesets/'; -} -sub relation{ - return 'http://purl.org/sipwise/ngcp-api/#rel-rewriterulesets'; +sub _set_config{ + my ($self, $method) = @_; + return { + own_transaction_control => { ALL => 1 }, + }; } -__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 }, - }, -); - -sub gather_default_action_roles { - my ($self, %args) = @_; my @roles = (); - push @roles, 'NGCP::Panel::Role::HTTPMethods' if $args{attributes}->{Method}; - return @roles; -} - -sub auto :Private { - my ($self, $c) = @_; - - $self->set_body($c); - $self->log_request($c); - return 1; -} - -sub GET :Allow { - my ($self, $c, $id) = @_; - { - last unless $self->valid_id($c, $id); - my $ruleset = $self->item_by_id($c, $id, "rulesets"); - last unless $self->resource_exists($c, ruleset => $ruleset); - - my $hal = $self->hal_from_item($c, $ruleset, "rewriterulesets"); - - 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"|r - =~ s/rel=self/rel="item self"/r; - } $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 allowed_methods{ + return [qw/GET OPTIONS HEAD PATCH PUT DELETE/]; } -sub PATCH :Allow { - my ($self, $c, $id) = @_; +sub delete_item { + my ($self, $c, $item) = @_; my $guard = $c->model('DB')->txn_scope_guard; { - my $preference = $self->require_preference($c); - last unless $preference; - - my $json = $self->get_valid_patch_data( - c => $c, - id => $id, - media_type => 'application/json-patch+json', - ops => [qw/add replace remove copy/], - ); - last unless $json; - - my $ruleset = $self->item_by_id($c, $id, "rulesets"); - last unless $self->resource_exists($c, ruleset => $ruleset); - my $old_resource = $self->hal_from_item($c, $ruleset, "rewriterulesets")->resource; - my $resource = $self->apply_patch($c, $old_resource, $json); - last unless $resource; - - my $form = $self->get_form($c); - $ruleset = $self->update_item($c, $ruleset, $old_resource, $resource, $form); - last unless $ruleset; - - $guard->commit; - NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); - - if ('minimal' eq $preference) { - $c->response->status(HTTP_NO_CONTENT); - $c->response->header(Preference_Applied => 'return=minimal'); - $c->response->body(q()); - } else { - my $hal = $self->hal_from_item($c, $ruleset, "rewriterulesets"); - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - $hal->http_headers, - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); - $c->response->body($response->content); + try { + $item->voip_rewrite_rules->delete; + $item->delete; + } catch($e) { + $c->log->error("Failed to delete rewriteruleset with id '".$item->id."': $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); + last; } + $guard->commit; + NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); } return; } -sub PUT :Allow { - my ($self, $c, $id) = @_; - my $guard = $c->model('DB')->txn_scope_guard; - { - my $preference = $self->require_preference($c); - last unless $preference; - - my $ruleset = $self->item_by_id($c, $id, "rulesets"); - last unless $self->resource_exists($c, ruleset => $ruleset); - my $resource = $self->get_valid_put_data( - c => $c, - id => $id, - media_type => 'application/json', - ); - last unless $resource; - my $old_resource = { $ruleset->get_inflated_columns }; - - my $form = $self->get_form($c); - $ruleset = $self->update_item($c, $ruleset, $old_resource, $resource, $form); - last unless $ruleset; - - $guard->commit; - NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); +sub update_item_model { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; - if ('minimal' eq $preference) { - $c->response->status(HTTP_NO_CONTENT); - $c->response->header(Preference_Applied => 'return=minimal'); - $c->response->body(q()); - } else { - my $hal = $self->hal_from_item($c, $ruleset, "rewriterulesets"); - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - $hal->http_headers, - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); - $c->response->body($response->content); - } - } - return; -} + my $id = delete $resource->{id}; -sub DELETE :Allow { - my ($self, $c, $id) = @_; my $guard = $c->model('DB')->txn_scope_guard; { - my $ruleset = $self->item_by_id($c, $id, "rulesets"); - last unless $self->resource_exists($c, ruleset => $ruleset); try { - $ruleset->voip_rewrite_rules->delete; - $ruleset->delete; + my $rewriterules = delete $resource->{rewriterules}; + $item->update($resource); + if ($rewriterules) { + $self->update_rewriterules( $c, $item, $rewriterules ); + } } catch($e) { - $c->log->error("Failed to delete rewriteruleset with id '$id': $e"); + $c->log->error("Failed to update rewriterule with id '$id': $e"); $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); - last; + die; } $guard->commit; NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); - - $c->response->status(HTTP_NO_CONTENT); - $c->response->body(q()); } - return; -} - -sub end : Private { - my ($self, $c) = @_; - $self->log_response($c); - return 1; + return $item; } - 1; # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/RewriteRules.pm b/lib/NGCP/Panel/Controller/API/RewriteRules.pm index e31bd03463..58dc0d52dd 100644 --- a/lib/NGCP/Panel/Controller/API/RewriteRules.pm +++ b/lib/NGCP/Panel/Controller/API/RewriteRules.pm @@ -1,20 +1,20 @@ package NGCP::Panel::Controller::API::RewriteRules; -use NGCP::Panel::Utils::Generic qw(:all); + +use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::RewriteRules/; use Sipwise::Base; +use NGCP::Panel::Utils::Generic qw(:all); -use boolean qw(true); -use NGCP::Panel::Utils::DataHal qw(); -use NGCP::Panel::Utils::DataHalLink qw(); -use HTTP::Headers qw(); use HTTP::Status qw(:constants); -use NGCP::Panel::Utils::DateTime; -use NGCP::Panel::Utils::Rewrite; -require Catalyst::ActionRole::ACL; -require Catalyst::ActionRole::CheckTrailingSlash; -require NGCP::Panel::Role::HTTPMethods; -require Catalyst::ActionRole::RequireSSL; +__PACKAGE__->set_config(); + +sub _set_config{ + my ($self, $method) = @_; + return { + own_transaction_control => { POST => 1 }, + }; +} sub allowed_methods{ return [qw/GET POST OPTIONS HEAD/]; @@ -29,24 +29,12 @@ sub query_params { { param => 'description', description => 'Filter rules for a certain description (wildcards possible).', - query => { - first => sub { - my $q = shift; - return { description => { like => $q } }; - }, - second => sub {}, - }, + query_type => 'string_like', }, { param => 'set_id', description => 'Filter for rules belonging to a specific rewriteruleset.', - query => { - first => sub { - my $q = shift; - return { set_id => $q }; - }, - second => sub {}, - }, + query_type => 'string_eq', }, { param => 'reseller_id', @@ -54,189 +42,33 @@ sub query_params { query => { first => sub { my $q = shift; - return { set_id => $q }; + return { 'ruleset.reseller_id' => $q }; }, - second => sub {}, + second => sub { join => 'ruleset' }, }, }, ]; } +sub create_item { + my ($self, $c, $resource, $form, $process_extras) = @_; + my $schema = $c->model('DB'); + my $guard = $schema->txn_scope_guard; -use parent qw/Catalyst::Controller NGCP::Panel::Role::API::RewriteRules/; - -sub resource_name{ - return 'rewriterules'; -} -sub dispatch_path{ - return '/api/rewriterules/'; -} -sub relation{ - return 'http://purl.org/sipwise/ngcp-api/#rel-rewriterules'; -} - -__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 }, - }, -); - -sub gather_default_action_roles { - my ($self, %args) = @_; my @roles = (); - push @roles, 'NGCP::Panel::Role::HTTPMethods' if $args{attributes}->{Method}; - return @roles; -} - -sub auto :Private { - my ($self, $c) = @_; + my $item; - $self->set_body($c); - $self->log_request($c); - return 1; -} - -sub GET :Allow { - my ($self, $c) = @_; - my $page = $c->request->params->{page} // 1; - my $rows = $c->request->params->{rows} // 10; - { - my $rules = $self->item_rs($c, "rules"); - - (my $total_count, $rules) = $self->paginate_order_collection($c, $rules); - my (@embedded, @links); - for my $rule ($rules->all) { - push @embedded, $self->hal_from_item($c, $rule, "rewriterules"); - push @links, NGCP::Panel::Utils::DataHalLink->new( - relation => 'ngcp:'.$self->resource_name, - href => sprintf('%s%d', $self->dispatch_path, $rule->id), - ); - } - push @links, - NGCP::Panel::Utils::DataHalLink->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - NGCP::Panel::Utils::DataHalLink->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - NGCP::Panel::Utils::DataHalLink->new(relation => 'self', href => sprintf('%s?page=%s&rows=%s', $self->dispatch_path, $page, $rows)); - if(($total_count / $rows) > $page ) { - push @links, NGCP::Panel::Utils::DataHalLink->new(relation => 'next', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page + 1, $rows)); - } - if($page > 1) { - push @links, NGCP::Panel::Utils::DataHalLink->new(relation => 'prev', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page - 1, $rows)); - } - - my $hal = NGCP::Panel::Utils::DataHal->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); + try { + $item = $schema->resultset('voip_rewrite_rules')->create($resource); + } catch($e) { + $c->log->error("failed to create rewriterule: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create rewriterule."); 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; + $guard->commit; + NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); + + return $item; } - -sub POST :Allow { - my ($self, $c) = @_; - - my $guard = $c->model('DB')->txn_scope_guard; - { - my $schema = $c->model('DB'); - my $resource = $self->get_valid_post_data( - c => $c, - media_type => 'application/json', - ); - last unless $resource; - - my $form = $self->get_form($c); - last unless $self->validate_form( - c => $c, - resource => $resource, - form => $form, - exceptions => [qw/set_id/], - ); - - $resource->{match_pattern} = $form->values->{match_pattern}; - $resource->{replace_pattern} = $form->values->{replace_pattern}; - - my $rule; - - unless(defined $resource->{set_id}) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Required: 'set_id'"); - last; - } - - my $reseller_id; - if($c->user->roles eq "reseller") { - $reseller_id = $c->user->reseller_id; - } - - my $ruleset = $schema->resultset('voip_rewrite_rule_sets')->find({ - id => $resource->{set_id}, - ($reseller_id ? (reseller_id => $reseller_id) : ()), - }); - unless($ruleset) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'set_id'."); - last; - } - try { - $rule = $schema->resultset('voip_rewrite_rules')->create($resource); - } catch($e) { - $c->log->error("failed to create rewriterule: $e"); # TODO: user, message, trace, ... - $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create rewriterule."); - last; - } - - $guard->commit; - NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); - - $c->response->status(HTTP_CREATED); - $c->response->header(Location => sprintf('/%s%d', $c->request->path, $rule->id)); - $c->response->body(q()); - } - return; -} - -sub end : Private { - my ($self, $c) = @_; - - $self->log_response($c); - return 1; -} - 1; # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/RewriteRulesItem.pm b/lib/NGCP/Panel/Controller/API/RewriteRulesItem.pm index 6f9b5a7073..ccf1d3284b 100644 --- a/lib/NGCP/Panel/Controller/API/RewriteRulesItem.pm +++ b/lib/NGCP/Panel/Controller/API/RewriteRulesItem.pm @@ -1,222 +1,60 @@ package NGCP::Panel::Controller::API::RewriteRulesItem; -use NGCP::Panel::Utils::Generic qw(:all); -use Sipwise::Base; +use parent qw/NGCP::Panel::Role::EntitiesItem NGCP::Panel::Role::API::RewriteRules/; -use boolean qw(true); -use NGCP::Panel::Utils::DataHal qw(); -use NGCP::Panel::Utils::DataHalLink qw(); -use HTTP::Headers qw(); +use Sipwise::Base; +use NGCP::Panel::Utils::Generic qw(:all); use HTTP::Status qw(:constants); -use NGCP::Panel::Utils::ValidateJSON qw(); -use NGCP::Panel::Utils::DateTime; -use NGCP::Panel::Utils::Rewrite; -use Path::Tiny qw(path); -use Safe::Isa qw($_isa); -require Catalyst::ActionRole::ACL; -require NGCP::Panel::Role::HTTPMethods; -require Catalyst::ActionRole::RequireSSL; +__PACKAGE__->set_config(); -sub allowed_methods{ - return [qw/GET OPTIONS HEAD PATCH PUT DELETE/]; +sub _set_config{ + my ($self, $method) = @_; + return { + own_transaction_control => { ALL => 1 }, + }; } -use parent qw/Catalyst::Controller NGCP::Panel::Role::API::RewriteRules/; - -sub resource_name{ - return 'rewriterules'; -} -sub dispatch_path{ - return '/api/rewriterules/'; -} -sub relation{ - return 'http://purl.org/sipwise/ngcp-api/#rel-rewriterules'; -} - -__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 }, - }, -); - -sub gather_default_action_roles { - my ($self, %args) = @_; my @roles = (); - push @roles, 'NGCP::Panel::Role::HTTPMethods' if $args{attributes}->{Method}; - return @roles; -} - -sub auto :Private { - my ($self, $c) = @_; - - $self->set_body($c); - $self->log_request($c); - return 1; -} - -sub GET :Allow { - my ($self, $c, $id) = @_; - { - last unless $self->valid_id($c, $id); - my $rule = $self->item_by_id($c, $id, "rules"); - last unless $self->resource_exists($c, rule => $rule); - - my $hal = $self->hal_from_item($c, $rule, "rewriterules"); - - 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"|r - =~ s/rel=self/rel="item self"/r; - } $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 allowed_methods{ + return [qw/GET OPTIONS HEAD PATCH PUT DELETE/]; } -sub PATCH :Allow { - my ($self, $c, $id) = @_; +sub delete_item { + my ($self, $c, $item) = @_; my $guard = $c->model('DB')->txn_scope_guard; { - my $preference = $self->require_preference($c); - last unless $preference; - - my $json = $self->get_valid_patch_data( - c => $c, - id => $id, - media_type => 'application/json-patch+json', - ops => [qw/add replace remove copy/], - ); - last unless $json; - - my $rule = $self->item_by_id($c, $id, "rules"); - last unless $self->resource_exists($c, rule => $rule); - my $old_resource = { $rule->get_inflated_columns }; - my $resource = $self->apply_patch($c, $old_resource, $json); - last unless $resource; - - my $form = $self->get_form($c); - $rule = $self->update_item($c, $rule, $old_resource, $resource, $form); - last unless $rule; - - $guard->commit; - NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); - - if ('minimal' eq $preference) { - $c->response->status(HTTP_NO_CONTENT); - $c->response->header(Preference_Applied => 'return=minimal'); - $c->response->body(q()); - } else { - my $hal = $self->hal_from_item($c, $rule, "rewriterules"); - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - $hal->http_headers, - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); - $c->response->body($response->content); + try { + $item->delete; + } catch($e) { + $c->log->error("Failed to delete rewriterule with id '".$item->id."': $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); + die; } - } - return; -} - -sub PUT :Allow { - my ($self, $c, $id) = @_; - my $guard = $c->model('DB')->txn_scope_guard; - { - my $preference = $self->require_preference($c); - last unless $preference; - - my $rule = $self->item_by_id($c, $id, "rules"); - last unless $self->resource_exists($c, rule => $rule); - my $resource = $self->get_valid_put_data( - c => $c, - id => $id, - media_type => 'application/json', - ); - last unless $resource; - my $old_resource = { $rule->get_inflated_columns }; - - my $form = $self->get_form($c); - $rule = $self->update_item($c, $rule, $old_resource, $resource, $form); - last unless $rule; - $guard->commit; NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); - - if ('minimal' eq $preference) { - $c->response->status(HTTP_NO_CONTENT); - $c->response->header(Preference_Applied => 'return=minimal'); - $c->response->body(q()); - } else { - my $hal = $self->hal_from_item($c, $rule, "rewriterules"); - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - $hal->http_headers, - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); - $c->response->body($response->content); - } } return; } -sub DELETE :Allow { - my ($self, $c, $id) = @_; +sub update_item_model { + my($self, $c, $item, $old_resource, $resource, $form, $process_extras) = @_; + + my $id = delete $resource->{id}; + my $guard = $c->model('DB')->txn_scope_guard; { - my $rule = $self->item_by_id($c, $id, "rules"); - last unless $self->resource_exists($c, rule => $rule); try { - $rule->delete; + $item->update($resource); } catch($e) { - $c->log->error("Failed to delete rewriterule with id '$id': $e"); + $c->log->error("Failed to update rewriterule with id '$id': $e"); $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); - last; + die; } $guard->commit; NGCP::Panel::Utils::Rewrite::sip_dialplan_reload($c); - - $c->response->status(HTTP_NO_CONTENT); - $c->response->body(q()); } - return; + return $item; } - -sub end : Private { - my ($self, $c) = @_; - - $self->log_response($c); - return 1; -} - 1; # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Field/RewriteRule.pm b/lib/NGCP/Panel/Field/RewriteRule.pm new file mode 100644 index 0000000000..e60cbfff19 --- /dev/null +++ b/lib/NGCP/Panel/Field/RewriteRule.pm @@ -0,0 +1,105 @@ +package NGCP::Panel::Field::RewriteRule; +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler::Field::Compound'; + +has_field 'match_pattern' => ( + type => '+NGCP::Panel::Field::Regexp', + required => 1, + inflate_default_method => \&inflate_match_pattern, + deflate_value_method => \&deflate_match_pattern, + element_attr => { + rel => ['tooltip'], + title => ['Match pattern, a regular expression.'], + }, +); + +has_field 'replace_pattern' => ( + type => 'Text', + required => 1, + label => 'Replacement Pattern', + inflate_default_method => \&inflate_replace_pattern, + deflate_value_method => \&deflate_replace_pattern, + element_attr => { + rel => ['tooltip'], + title => ['Replacement pattern.'], + }, +); + +has_field 'description' => ( + type => 'Text', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['Arbitrary text.'], + }, +); + +has_field 'direction' => ( + type => 'Select', + options => [ + { label => 'Inbound', value => 'in'}, + { label => 'Outbound', value => 'out'}, + { label => 'LNP', value => 'lnp'}, + ], + element_attr => { + rel => ['tooltip'], + title => ['Inbound (in), Outbound (out) or LNP (lnp).'] + }, +); + +has_field 'enabled' => ( + type => 'Boolean', + label => 'Enabled', + default => 1, + element_attr => { + rel => ['tooltip'], + title => ['Rule enabled state.'], + }, +); + +has_field 'field' => ( + type => 'Select', + options => [ + { label => 'Callee', value => 'callee'}, + { label => 'Caller', value => 'caller'}, + ], + element_attr => { + rel => ['tooltip'], + title => ['caller or callee.'] + }, +); + +sub deflate_match_pattern { + my ($self, $value) = @_; + + $value =~ s/\$\{(\w+)\}/\$avp(s:$1)/g; + $value =~ s/\@\{(\w+)\}/\$(avp(s:$1)[+])/g; + return $value; +}; + +sub inflate_match_pattern { + my ($self, $value) = @_; + + $value =~ s/\$avp\(s\:(\w+)\)/\${$1}/g; + $value =~ s/\$\(avp\(s\:(\w+)\)\[\+\]\)/\@{$1}/g; + return $value; +} + +sub deflate_replace_pattern { + my ($self, $value) = @_; + + $value =~ s/\$\{(\w+)\}/\$avp(s:$1)/g; + return $value; +}; + +sub inflate_replace_pattern { + my ($self, $value) = @_; + + $value =~ s/\$avp\(s\:(\w+)\)/\${$1}/g; + return $value; +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Field/RewriteRuleAPI.pm b/lib/NGCP/Panel/Field/RewriteRuleAPI.pm new file mode 100644 index 0000000000..c825b15341 --- /dev/null +++ b/lib/NGCP/Panel/Field/RewriteRuleAPI.pm @@ -0,0 +1,18 @@ +package NGCP::Panel::Field::RewriteRuleAPI; +use Moose; +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Field::RewriteRule'; + +has_field 'priority' => ( + type => 'PosInteger', + required => 0, + element_attr => { + rel => ['tooltip'], + title => ['The rewrite rule priority.'] + }, +); + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/RewriteRule/AdminSetAPI.pm b/lib/NGCP/Panel/Form/RewriteRule/AdminSetAPI.pm new file mode 100644 index 0000000000..3a73a4167a --- /dev/null +++ b/lib/NGCP/Panel/Form/RewriteRule/AdminSetAPI.pm @@ -0,0 +1,23 @@ +package NGCP::Panel::Form::RewriteRule::AdminSetAPI; + +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::RewriteRule::ResellerSetAPI'; + +has_field 'reseller_id' => ( + type => 'PosInteger', + validate_when_empty => 1, + element_attr => { + rel => ['tooltip'], + title => ['The reseller who can use the Ruleset.'], + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/reseller_id name description rewriterules/], +); + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/RewriteRule/ResellerSetAPI.pm b/lib/NGCP/Panel/Form/RewriteRule/ResellerSetAPI.pm new file mode 100644 index 0000000000..2eb0dc964c --- /dev/null +++ b/lib/NGCP/Panel/Form/RewriteRule/ResellerSetAPI.pm @@ -0,0 +1,29 @@ +package NGCP::Panel::Form::RewriteRule::ResellerSetAPI; + +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::RewriteRule::ResellerSet'; + +has_field 'rewriterules' => ( + type => 'Repeatable', + required => 0, + setup_for_js => 1, + do_wrapper => 1, + do_label => 0, + wrapper_class => [qw/hfh-rep/], + element_attr => { + rel => ['tooltip'], + title => ['Rewrite Rules'], + }, +); +has_field 'rewriterules.contains' => ( type => '+NGCP::Panel::Field::RewriteRuleAPI' ); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/name description rewriterules/], +); + + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/RewriteRule/RuleAPI.pm b/lib/NGCP/Panel/Form/RewriteRule/RuleAPI.pm index f76a975a4e..6f5e148256 100644 --- a/lib/NGCP/Panel/Form/RewriteRule/RuleAPI.pm +++ b/lib/NGCP/Panel/Form/RewriteRule/RuleAPI.pm @@ -12,10 +12,19 @@ has_field 'set_id' => ( }, ); +has_field 'priority' => ( + type => 'PosInteger', + required => 0, + element_attr => { + rel => ['tooltip'], + title => ['The rewrite rule priority.'] + }, +); + has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/set_id match_pattern replace_pattern description direction enabled field/], + render_list => [qw/set_id match_pattern replace_pattern description direction enabled priority field/], ); 1; diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index d8f90878e5..45e18aee8f 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -849,7 +849,6 @@ sub hal_from_item { $form_exceptions = $params->{form_exceptions}; } my $resource = $self->resource_from_item($c, $item, $form); - $resource = $self->process_hal_resource($c, $item, $resource, $form); my $links = $self->hal_links($c, $item, $resource, $form) // []; my $hal = NGCP::Panel::Utils::DataHal->new( @@ -878,6 +877,7 @@ sub hal_from_item { ); } $resource->{id} = $self->get_item_id($c, $item); + $resource = $self->post_process_hal_resource($c, $item, $resource, $form); $hal->resource({%$resource}); return $hal; } @@ -888,10 +888,13 @@ sub update_item { ($form, $form_exceptions, $process_extras) = @{$params}{qw/form form_exceptions process_extras/}; if(!$form){ - ($form, $form_exceptions) = $self->get_form($c); + ($form, $form_exceptions) = $self->get_form($c, 'edit'); } if($form){ + if(!$form_exceptions && $form->can('form_exceptions')){ + $form_exceptions = $form->form_exceptions; + } return unless $self->validate_form( c => $c, resource => $resource, @@ -964,6 +967,11 @@ sub process_hal_resource { return $resource; } +sub post_process_hal_resource { + my($self, $c, $item, $resource, $form) = @_; + return $resource; +} + sub hal_links { my($self, $c, $item, $resource, $form) = @_; return []; @@ -1002,7 +1010,55 @@ sub update_item_model{ return $item; } +sub post_process_commit{ + my($self, $c, $action, $item, $old_resource, $resource, $form, $process_extras) = @_; + return; +} +sub check_transaction_control{ + my($self, $c, $action, $step, %params) = @_; + my $res = 1; + my $config = $self->config->{own_transaction_control}; + if(!$config){ + $res = 1; + }else{ + if($config->{ALL}){ + $res = 0; + }elsif( ('HASH' eq $self->config->{own_transaction_control}->{$action} && $self->config->{own_transaction_control}->{$action}->{$step} ) + || $self->config->{own_transaction_control}->{$action}){ + $res = 0; + } + } + return $res; +} + +sub get_transaction_control{ + my $self = shift; + my($c, $action, $step, %params) = @_; + my $schema = $params{schema} // $c->model('DB'); + $action //= uc $c->request->method; + $step //= 'init'; + if($self->check_transaction_control($c, $action, $step, %params)){ + #todo: put it into class variables? + $c->stash->{transaction_quard} = $schema->txn_scope_guard; + return $c->stash->{transaction_quard}; + } + return; +} + +sub complete_transaction{ + my $self = shift; + my($c, $action, $step, %params) = @_; + my $schema = $params{schema} // $c->model('DB'); + my $guard = $params{guard} // $c->stash->{transaction_quard}; + $action //= uc $c->request->method; + $step //= 'commit'; + if($self->check_transaction_control($c, $action, $step, %params)){ + $guard->commit; + $c->stash->{transaction_quard} = undef; + } + return; +} #------ accessors --- sub dispatch_path { diff --git a/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm b/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm index f98379fe65..266becfb6d 100644 --- a/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm +++ b/lib/NGCP/Panel/Role/API/RewriteRuleSets.pm @@ -1,41 +1,48 @@ package NGCP::Panel::Role::API::RewriteRuleSets; -use NGCP::Panel::Utils::Generic qw(:all); + +use parent qw/NGCP::Panel::Role::API/; use Sipwise::Base; +use NGCP::Panel::Utils::Generic qw(:all); +use HTTP::Status qw(:constants); -use parent 'NGCP::Panel::Role::API'; +use NGCP::Panel::Form::RewriteRule::AdminSetAPI; +use NGCP::Panel::Form::RewriteRule::ResellerSet; +sub item_name{ + return 'rewriteruleset'; +} -use boolean qw(true); -use NGCP::Panel::Utils::DataHal qw(); -use NGCP::Panel::Utils::DataHalLink qw(); -use HTTP::Status qw(:constants); -use JSON::Types; -use NGCP::Panel::Form::RewriteRule::AdminSet; -use NGCP::Panel::Form::RewriteRule::ResellerSet; -use NGCP::Panel::Form::RewriteRule::Rule; +sub resource_name{ + return 'rewriterulesets'; +} + +sub dispatch_path{ + return '/api/rewriterulesets/'; +} + +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-rewriterulesets'; +} sub get_form { my ($self, $c, $type) = @_; if ($type && $type eq "rules") { - return NGCP::Panel::Form::RewriteRule::Rule->new; + return (NGCP::Panel::Form::RewriteRule::RuleAPI->new); } if($c->user->roles eq "admin") { - return NGCP::Panel::Form::RewriteRule::AdminSet->new; + return (NGCP::Panel::Form::RewriteRule::AdminSetAPI->new( ctx => $c), [qw/reseller_id/]); } else { - return NGCP::Panel::Form::RewriteRule::ResellerSet->new; + return (NGCP::Panel::Form::RewriteRule::ResellerSet->new( ctx => $c), [qw/reseller_id/]); } } -sub hal_from_item { - my ($self, $c, $item, $type) = @_; - my $form; +sub post_process_hal_resource { + my($self, $c, $item, $resource, $form) = @_; my $rwr_form = $self->get_form($c, "rules"); - - my %resource = $item->get_inflated_columns; my @rewriterules; - for my $rule ( $item->voip_rewrite_rules->all ) { + for my $rule ( $item->voip_rewrite_rules->search_rs(undef, { order_by => { '-asc' => 'priority' } } )->all ) { my $rule_resource = { $rule->get_inflated_columns }; return unless $self->validate_form( c => $c, @@ -43,71 +50,45 @@ sub hal_from_item { resource => $rule_resource, run => 0, ); + delete $rule_resource->{set_id}; push @rewriterules, $rule_resource; } - - my $hal = NGCP::Panel::Utils::DataHal->new( - links => [ - NGCP::Panel::Utils::DataHalLink->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - NGCP::Panel::Utils::DataHalLink->new(relation => 'collection', href => sprintf("%s", $self->dispatch_path)), - NGCP::Panel::Utils::DataHalLink->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - NGCP::Panel::Utils::DataHalLink->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), - NGCP::Panel::Utils::DataHalLink->new(relation => "ngcp:$type", href => sprintf("/api/%s/%d", $type, $item->id)), - ], - relation => 'ngcp:'.$self->resource_name, - ); - - $form //= $self->get_form($c); - return unless $self->validate_form( - c => $c, - form => $form, - resource => \%resource, - run => 0, - ); - $resource{rewriterules} = \@rewriterules; - $hal->resource(\%resource); - return $hal; + $resource->{rewriterules} = \@rewriterules; + return $resource; } sub _item_rs { my ($self, $c, $type) = @_; my $item_rs; - if($type eq "rulesets") { - if($c->user->roles eq "admin") { - $item_rs = $c->model('DB')->resultset('voip_rewrite_rule_sets'); - } elsif($c->user->roles eq "reseller") { - $item_rs = $c->model('DB')->resultset('voip_rewrite_rule_sets') - ->search_rs({reseller_id => $c->user->reseller_id}); - } - } else { - die "You should not reach this"; + if($c->user->roles eq "admin") { + $item_rs = $c->model('DB')->resultset('voip_rewrite_rule_sets'); + } elsif($c->user->roles eq "reseller") { + $item_rs = $c->model('DB')->resultset('voip_rewrite_rule_sets') + ->search_rs({reseller_id => $c->user->reseller_id}); } return $item_rs; } -sub item_by_id { - my ($self, $c, $id, $type) = @_; - - my $item_rs = $self->item_rs($c, $type); - return $item_rs->find($id); -} - -sub update_item { - my ($self, $c, $item, $old_resource, $resource, $form) = @_; - - delete $resource->{id}; - - if($c->user->roles eq "reseller") { - $resource->{reseller_id} = $old_resource->{reseller_id}; # prohibit change +sub process_form_resource{ + my($self,$c, $item, $old_resource, $resource, $form, $process_extras) = @_; + my $reseller_id; + if($c->user->roles eq "admin") { + try { + $reseller_id = $resource->{reseller_id} + || $c->user->contract->contact->reseller_id; + } + } elsif($c->user->roles eq "reseller") { + $reseller_id = $c->user->reseller_id; } + $resource->{reseller_id} = $reseller_id; + return $resource; +} - if($old_resource->{reseller_id} != $resource->{reseller_id}) { +sub check_resource{ + my($self, $c, $item, $old_resource, $resource, $form, $process_extras) = @_; + my $schema = $c->model('DB'); + if(!$old_resource || ( $old_resource->{reseller_id} != $resource->{reseller_id}) ) { my $reseller = $c->model('DB')->resultset('resellers') ->find($resource->{reseller_id}); unless($reseller) { @@ -115,28 +96,47 @@ sub update_item { return; } } + return 1; +} + +sub check_duplicate{ + my($self, $c, $item, $old_resource, $resource, $form, $process_extras) = @_; - if ($resource->{rewriterules}) { - $item->voip_rewrite_rules->delete; - my $i = 30; - for my $rule (@{ $resource->{rewriterules} }) { + my $schema = $c->model('DB'); + my $existing_item = $schema->resultset('voip_rewrite_rule_sets')->search_rs({ + name => $resource->{name} + })->first; + if ($existing_item && (!$item || $item->id != $existing_item->id)) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Ruleset with this 'name' already exists."); + return; + } + return 1; +} + +sub update_rewriterules{ + my($self, $c, $item, $rewriterules ) = @_; + + my $schema = $c->model('DB'); + + my $priority = 30; + if (ref($rewriterules) ne "ARRAY") { + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "rewriterules must be an array."); + die; + } + $item->voip_rewrite_rules->delete; + for my $rule (@{ $rewriterules }) { + try { $item->voip_rewrite_rules->create({ + priority => $priority++, %{ $rule }, - priority => $i++, }); + } catch($e) { + $c->log->error("failed to create rewriterules: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create rewrite rules."); + die; } } - return unless $self->validate_form( - c => $c, - form => $form, - resource => $resource, - ); - #TODO: priority not accessible here - $item->update($resource); - - return $item; } - 1; # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/RewriteRules.pm b/lib/NGCP/Panel/Role/API/RewriteRules.pm index db29b6e42d..15abfe986b 100644 --- a/lib/NGCP/Panel/Role/API/RewriteRules.pm +++ b/lib/NGCP/Panel/Role/API/RewriteRules.pm @@ -1,110 +1,103 @@ package NGCP::Panel::Role::API::RewriteRules; -use NGCP::Panel::Utils::Generic qw(:all); + +use parent qw/NGCP::Panel::Role::API/; use Sipwise::Base; +use NGCP::Panel::Utils::Generic qw(:all); +use HTTP::Status qw(:constants); -use parent 'NGCP::Panel::Role::API'; +use NGCP::Panel::Form::RewriteRule::RuleAPI; +sub item_name{ + return 'rewriterule'; +} -use boolean qw(true); -use NGCP::Panel::Utils::DataHal qw(); -use NGCP::Panel::Utils::DataHalLink qw(); -use HTTP::Status qw(:constants); -use JSON::Types; -use NGCP::Panel::Form::RewriteRule::AdminSet; -use NGCP::Panel::Form::RewriteRule::ResellerSet; -use NGCP::Panel::Form::RewriteRule::RuleAPI; +sub resource_name{ + return 'rewriterules'; +} -sub get_form { - my ($self, $c) = @_; - return NGCP::Panel::Form::RewriteRule::RuleAPI->new; +sub dispatch_path{ + return '/api/rewriterules/'; } -sub hal_from_item { - my ($self, $c, $item, $type) = @_; - my $form; - - my %resource = $item->get_inflated_columns; - - my $hal = NGCP::Panel::Utils::DataHal->new( - links => [ - NGCP::Panel::Utils::DataHalLink->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - NGCP::Panel::Utils::DataHalLink->new(relation => 'collection', href => sprintf("%s", $self->dispatch_path)), - NGCP::Panel::Utils::DataHalLink->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - NGCP::Panel::Utils::DataHalLink->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), - NGCP::Panel::Utils::DataHalLink->new(relation => "ngcp:$type", href => sprintf("/api/%s/%d", $type, $item->id)), - NGCP::Panel::Utils::DataHalLink->new(relation => "ngcp:rewriterulesets", href => sprintf("/api/rewriterulesets/%d", $item->set_id)), - ], - relation => 'ngcp:'.$self->resource_name, - ); - - $form //= $self->get_form($c); - return unless $self->validate_form( - c => $c, - form => $form, - resource => \%resource, - run => 0, - exceptions => [qw/set_id/], - ); - - $resource{match_pattern} = $form->inflate_match_pattern($resource{match_pattern}); - $resource{replace_pattern} = $form->inflate_replace_pattern($resource{replace_pattern}); - - $hal->resource(\%resource); - return $hal; +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-rewriterules'; +} + +sub config_allowed_roles { + return [qw/admin reseller/]; } sub _item_rs { my ($self, $c, $type) = @_; my $item_rs; - if($type eq "rules") { - if($c->user->roles eq "admin") { - $item_rs = $c->model('DB')->resultset('voip_rewrite_rules'); - } elsif ($c->user->roles eq "reseller") { - $item_rs = $c->model('DB')->resultset('voip_rewrite_rules')->search_rs({ - 'ruleset.reseller_id' => $c->user->reseller_id, - },{ - join => 'ruleset' - }); - } - } else { - die "You should not reach this"; + if($c->user->roles eq "admin") { + $item_rs = $c->model('DB')->resultset('voip_rewrite_rules'); + } elsif ($c->user->roles eq "reseller") { + $item_rs = $c->model('DB')->resultset('voip_rewrite_rules')->search_rs({ + 'ruleset.reseller_id' => $c->user->reseller_id, + },{ + join => 'ruleset' + }); } return $item_rs; } -sub item_by_id { - my ($self, $c, $id, $type) = @_; - - my $item_rs = $self->item_rs($c, $type); - return $item_rs->find($id); +sub get_form { + my ($self, $c) = @_; + return ( NGCP::Panel::Form::RewriteRule::RuleAPI->new, ['set_id'] ); } -sub update_item { - my ($self, $c, $item, $old_resource, $resource, $form) = @_; - - delete $resource->{id}; +sub hal_links{ + my($self, $c, $item, $resource, $form) = @_; + return [ + NGCP::Panel::Utils::DataHalLink->new(relation => "ngcp:rewriterulesets", href => sprintf("/api/rewriterulesets/%d", $item->set_id)), + ]; +} - return unless $self->validate_form( - c => $c, - form => $form, - resource => $resource, - exceptions => [qw/set_id/], - ); +sub post_process_hal_resource { + my($self, $c, $item, $resource, $form) = @_; + $resource->{match_pattern} = $form->inflate_match_pattern($resource->{match_pattern}); + $resource->{replace_pattern} = $form->inflate_replace_pattern($resource->{replace_pattern}); + return $resource; +} +sub process_form_resource{ + my($self,$c, $item, $old_resource, $resource, $form, $process_extras) = @_; $resource->{match_pattern} = $form->values->{match_pattern}; $resource->{replace_pattern} = $form->values->{replace_pattern}; + return $resource; +} + +sub check_resource{ + my($self, $c, $item, $old_resource, $resource, $form, $process_extras) = @_; + my $schema = $c->model('DB'); - $item->update($resource); + unless(defined $resource->{set_id}) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Required: 'set_id'"); + return; + } + + my $reseller_id; + if($c->user->roles eq "reseller") { + $reseller_id = $c->user->reseller_id; + } - return $item; + my $ruleset = $schema->resultset('voip_rewrite_rule_sets')->find({ + id => $resource->{set_id}, + ($reseller_id ? (reseller_id => $reseller_id) : ()), + }); + unless($ruleset) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'set_id'."); + return; + } + + $c->stash->{checked}->{ruleset} = $ruleset; + + return 1; } + 1; # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/Entities.pm b/lib/NGCP/Panel/Role/Entities.pm index 38e654e294..a185f62cdf 100644 --- a/lib/NGCP/Panel/Role/Entities.pm +++ b/lib/NGCP/Panel/Role/Entities.pm @@ -30,6 +30,8 @@ sub set_config { }, #action_roles => [qw(HTTPMethods)], %{$self->_set_config()}, + #log_response = 0|1 - don't log response body + #own_transaction_control->{PUT|POST|PATCH|DELETE} = 0|1 - don't start transaction guard ); } @@ -105,7 +107,7 @@ sub get { sub post { my ($self) = shift; my ($c) = @_; - my $guard = $c->model('DB')->txn_scope_guard; + my $guard = $self->get_transaction_control($c); { my $method_config = $self->config->{action}->{POST}; my ($resource) = $self->get_valid_data( @@ -115,7 +117,11 @@ sub post { uploads => $method_config->{Uploads} // [] , ); last unless $resource; - my ($form, $form_exceptions) = $self->get_form($c); + #instead of type parameter get_form can check request method + my ($form, $form_exceptions) = $self->get_form($c, 'add'); + if(!$form_exceptions && $form->can('form_exceptions')){ + $form_exceptions = $form->form_exceptions; + } last unless $self->validate_form( c => $c, resource => $resource, @@ -132,8 +138,10 @@ sub post { my $item = $self->create_item($c, $resource, $form, $process_extras); last unless $item; - $guard->commit; + $self->complete_transaction($c); + $self->post_process_commit($c, 'create', $item, undef, $resource, $form, $process_extras); + $self->return_representation_post($c, 'item' => $item, 'form' => $form, 'form_exceptions' => $form_exceptions ); } return; diff --git a/lib/NGCP/Panel/Role/EntitiesItem.pm b/lib/NGCP/Panel/Role/EntitiesItem.pm index ca99a3c1d3..c641b2c2ff 100644 --- a/lib/NGCP/Panel/Role/EntitiesItem.pm +++ b/lib/NGCP/Panel/Role/EntitiesItem.pm @@ -35,6 +35,8 @@ sub set_config { #action_roles => [qw(HTTPMethods)], log_response => 1, %{$self->_set_config()}, + #log_response = 0|1 - don't log response body + #own_transaction_control = {post|put|patch|delete|all => 1|0} ); } @@ -54,7 +56,10 @@ sub get { my $item = $self->item_by_id_valid($c, $id); last unless $item; my $header_accept = $c->request->header('Accept'); - if(defined $header_accept && ($header_accept ne 'application/json')) { + if(defined $header_accept + && ($header_accept ne 'application/json') + && ($header_accept ne '*/*') + ) { $self->return_requested_type($c,$id,$item); return; } @@ -76,7 +81,7 @@ sub get { sub patch { my ($self, $c, $id) = @_; - my $guard = $c->model('DB')->txn_scope_guard; + my $guard = $self->get_transaction_control($c); { my $preference = $self->require_preference($c); last unless $preference; @@ -94,11 +99,14 @@ sub patch { my $resource = $self->apply_patch($c, $old_resource, $json); last unless $resource; - my $form; - ($item,$form) = $self->update_item($c, $item, $old_resource, $resource, $form); + my ($form, $form_exceptions, $process_extras); + + ($item, $form, $form_exceptions, $process_extras) = $self->update_item($c, $item, $old_resource, $resource, $form, $process_extras ); last unless $item; - $guard->commit; + $self->complete_transaction($c); + $self->post_process_commit($c, 'patch', $item, $old_resource, $resource, $form, $process_extras); + $self->return_representation($c, 'item' => $item, 'form' => $form, 'preference' => $preference ); } return; @@ -106,7 +114,7 @@ sub patch { sub put { my ($self, $c, $id) = @_; - my $guard = $c->model('DB')->txn_scope_guard; + my $guard = $self->get_transaction_control($c); { my $preference = $self->require_preference($c); last unless $preference; @@ -124,12 +132,13 @@ sub put { last unless $resource; my $old_resource = { $item->get_inflated_columns }; #TODO: MOVE form exceptions to proper forms as property - my ($form, $form_exceptions); + my ($form, $form_exceptions, $process_extras); - ($item, $form, $form_exceptions) = $self->update_item($c, $item, $old_resource, $resource, $form ); + ($item, $form, $form_exceptions, $process_extras) = $self->update_item($c, $item, $old_resource, $resource, $form, $process_extras ); last unless $item; - $guard->commit; + $self->complete_transaction($c); + $self->post_process_commit($c, 'put', $item, $old_resource, $resource, $form, $process_extras); $self->return_representation($c, 'item' => $item, 'form' => $form, 'preference' => $preference, 'form_exceptions' => $form_exceptions ); } @@ -140,13 +149,15 @@ sub put { sub delete { my ($self, $c, $id) = @_; - my $guard = $c->model('DB')->txn_scope_guard; + my $guard = $self->get_transaction_control($c); { my $item = $self->item_by_id_valid($c, $id); last unless $item; $self->delete_item($c, $item ); - $guard->commit; + + $self->complete_transaction($c); + $self->post_process_commit($c, 'delete', $item); $c->response->status(HTTP_NO_CONTENT); $c->response->body(q()); diff --git a/t/api-rest/api-rewriterules.t b/t/api-rest/api-rewriterules.t index fb1d9a9bc0..4bd9b9014f 100644 --- a/t/api-rest/api-rewriterules.t +++ b/t/api-rest/api-rewriterules.t @@ -41,5 +41,94 @@ $test_machine->check_bundle(); $test_machine->clear_test_data_all(); done_testing; +__DATA__ + + # try to create rule with invalid set_id + $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + set_id => 999999, + description => "test rule $t", + direction => "in", + field => "caller", + match_pattern => "test pattern $t", + replace_pattern => "test_replace_$t", + })); + $res = $ua->request($req); + is($res->code, 422, "create rule with invalid set_id"); + $err = JSON::from_json($res->decoded_content); + is($err->{code}, "422", "check error code in body"); + like($err->{message}, qr/Invalid 'set_id'/, "check error message in body"); + + # try to create rule with negative set_id + $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + set_id => -100, + description => "test rule $t", + direction => "in", + field => "caller", + match_pattern => "test pattern $t", + replace_pattern => "test_replace_$t", + })); + $res = $ua->request($req); + is($res->code, 422, "create rule with negative set_id"); + $err = JSON::from_json($res->decoded_content); + is($err->{code}, "422", "check error code in body"); + like($err->{message}, qr/(Invalid|Validation failed).*'set_id'/, "check error message in body"); + + # try to create rule with missing match_pattern + $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + set_id => $rewriteruleset_id, + description => "test rule $t", + direction => "in", + field => "caller", + #match_pattern => "test pattern $t", + replace_pattern => "test_replace_$t", + })); + $res = $ua->request($req); + is($res->code, 422, "create rule with missing match_pattern"); + $err = JSON::from_json($res->decoded_content); + is($err->{code}, "422", "check error code in body"); + like($err->{message}, qr/field='match_pattern'/, "check error message in body"); + + # try to create rule with invalid direction and field + $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + set_id => $rewriteruleset_id, + description => "test rule $t", + direction => "foo", + field => "bar", + match_pattern => "test pattern $t", + replace_pattern => "test_replace_$t", + })); + $res = $ua->request($req); + is($res->code, 422, "create rule with invalid direction and field"); + $err = JSON::from_json($res->decoded_content); + is($err->{code}, "422", "check error code in body"); + like($err->{message}, qr/field='direction'/, "check error message in body"); + like($err->{message}, qr/field='field'/, "check error message in body"); + + # try to create rule without set_id + $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + #set_id => $rewriteruleset_id, + description => "test rule $t", + direction => "in", + field => "caller", + match_pattern => "test pattern $t", + replace_pattern => "test_replace_$t", + })); + $res = $ua->request($req); + is($res->code, 422, "create rule without set_id"); + $err = JSON::from_json($res->decoded_content); + is($err->{code}, "422", "check error code in body"); + like($err->{message}, qr/Required: 'set_id'|set_id.*required/, "check error message in body"); + + # vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-rewriterulesets.t b/t/api-rest/api-rewriterulesets.t index 0c872ae754..97980562f2 100644 --- a/t/api-rest/api-rewriterulesets.t +++ b/t/api-rest/api-rewriterulesets.t @@ -1,3 +1,102 @@ +use strict; +use warnings; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; + +#init test_machine +my $test_machine = Test::Collection->new( + name => 'rewriterulesets', + embedded_resources => [] +); +$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)}; + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + rewriterulesets => { + 'data' => { + reseller_id => sub { return shift->get_id('resellers',@_); }, + name => 'api_test', + description => 'api_test rule set description', + caller_in_dpid => '1', + callee_in_dpid => '2', + caller_out_dpid => '3', + callee_out_dpid => '4', + rewriterules => [{ + match_pattern => '^1111$', + replace_pattern => '2221', + description => 'test_api rewrite rule 1', + direction => 'in',#out + field => 'caller',#calee + priority => '2', + enabled => '1', + },{ + match_pattern => '^1112$', + replace_pattern => '2222', + description => 'test_api rewrite rule 2', + direction => 'in',#out + field => 'caller',#calee + priority => '3', + enabled => '1', + },{ + match_pattern => '^1113$', + replace_pattern => '2223', + description => 'test_api rewrite rule 3', + direction => 'in',#out + field => 'caller',#calee + priority => '1', + enabled => '1', + }, + ], + }, + 'query' => ['name'], + 'uniquizer_cb' => sub { Test::FakeData::string_uniquizer(\$_[0]->{name}); }, + }, +}); + +#for item creation test purposes /post request data/ +$test_machine->DATA_ITEM_STORE($fake_data->process('rewriterulesets')); + +$test_machine->form_data_item( ); +# create 3 new field pbx devices from DATA_ITEM +my $sets = $test_machine->check_create_correct( 3, sub{ $_[0]->{name} .= $_[1]->{i}.time(); } ); +$test_machine->check_get2put(); +$test_machine->check_bundle(); +print Dumper $sets; +for(my $i=0; $i < scalar @$sets; $i++){ + my $set = $sets->[$i]; + my $rewriterules = $sets->[$i]->{content}->{rewriterules}; + my $priority = -1; + for(my $j=0; $j < scalar @{$rewriterules}; $j++){ + my $priority_new = $rewriterules->[$j]->{priority}; + diag("Check priority order $i:$j: $priority < $priority_new"); + ok($priority < $priority_new) ; + } +} + +# try to create model without reseller_id +{ + my ($res, $err) = $test_machine->check_item_post(sub{delete $_[0]->{reseller_id};}); + is($res->code, 422, "create model without reseller_id"); + is($err->{code}, "422", "check error code in body"); + ok($err->{message} =~ /field='reseller_id'/, "check error message in body"); +} + + +$test_machine->clear_test_data_all(); + +done_testing; + +__DATA__ + + + + + + use warnings; use strict; @@ -13,24 +112,7 @@ my ($ua, $req, $res); use Test::Collection; $ua = Test::Collection->new()->ua(); -# OPTIONS tests -{ - $req = HTTP::Request->new('OPTIONS', $uri.'/api/rewriterulesets/'); - $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-rewriterulesets", "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 POST )) { - 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 $reseller_id = 1; - -# first, we create a rewriteruleset +t, we create a rewriteruleset $req = HTTP::Request->new('POST', $uri.'/api/rewriterulesets/'); $req->header('Content-Type' => 'application/json'); $req->header('Prefer' => 'return=representation'); @@ -104,92 +186,6 @@ my @allrules = (); is($err->{code}, "422", "check error code in body"); like($err->{message}, qr/Invalid 'reseller_id'/, "check error message in body"); - # try to create rule with invalid set_id - $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); - $req->header('Content-Type' => 'application/json'); - $req->content(JSON::to_json({ - set_id => 999999, - description => "test rule $t", - direction => "in", - field => "caller", - match_pattern => "test pattern $t", - replace_pattern => "test_replace_$t", - })); - $res = $ua->request($req); - is($res->code, 422, "create rule with invalid set_id"); - $err = JSON::from_json($res->decoded_content); - is($err->{code}, "422", "check error code in body"); - like($err->{message}, qr/Invalid 'set_id'/, "check error message in body"); - - # try to create rule with negative set_id - $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); - $req->header('Content-Type' => 'application/json'); - $req->content(JSON::to_json({ - set_id => -100, - description => "test rule $t", - direction => "in", - field => "caller", - match_pattern => "test pattern $t", - replace_pattern => "test_replace_$t", - })); - $res = $ua->request($req); - is($res->code, 422, "create rule with negative set_id"); - $err = JSON::from_json($res->decoded_content); - is($err->{code}, "422", "check error code in body"); - like($err->{message}, qr/(Invalid|Validation failed).*'set_id'/, "check error message in body"); - - # try to create rule with missing match_pattern - $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); - $req->header('Content-Type' => 'application/json'); - $req->content(JSON::to_json({ - set_id => $rewriteruleset_id, - description => "test rule $t", - direction => "in", - field => "caller", - #match_pattern => "test pattern $t", - replace_pattern => "test_replace_$t", - })); - $res = $ua->request($req); - is($res->code, 422, "create rule with missing match_pattern"); - $err = JSON::from_json($res->decoded_content); - is($err->{code}, "422", "check error code in body"); - like($err->{message}, qr/field='match_pattern'/, "check error message in body"); - - # try to create rule with invalid direction and field - $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); - $req->header('Content-Type' => 'application/json'); - $req->content(JSON::to_json({ - set_id => $rewriteruleset_id, - description => "test rule $t", - direction => "foo", - field => "bar", - match_pattern => "test pattern $t", - replace_pattern => "test_replace_$t", - })); - $res = $ua->request($req); - is($res->code, 422, "create rule with invalid direction and field"); - $err = JSON::from_json($res->decoded_content); - is($err->{code}, "422", "check error code in body"); - like($err->{message}, qr/field='direction'/, "check error message in body"); - like($err->{message}, qr/field='field'/, "check error message in body"); - - # try to create rule without set_id - $req = HTTP::Request->new('POST', $uri.'/api/rewriterules/'); - $req->header('Content-Type' => 'application/json'); - $req->content(JSON::to_json({ - #set_id => $rewriteruleset_id, - description => "test rule $t", - direction => "in", - field => "caller", - match_pattern => "test pattern $t", - replace_pattern => "test_replace_$t", - })); - $res = $ua->request($req); - is($res->code, 422, "create rule without set_id"); - $err = JSON::from_json($res->decoded_content); - is($err->{code}, "422", "check error code in body"); - like($err->{message}, qr/Required: 'set_id'|set_id.*required/, "check error message in body"); - # iterate over rules collection to check next/prev links and status my $nexturi = $uri.'/api/rewriterules/?page=1&rows=5'; do {