From 2c54106fd0bfcfedee6794568d5e89952b519463 Mon Sep 17 00:00:00 2001 From: Andreas Granig Date: Wed, 3 Jun 2015 18:06:36 +0200 Subject: [PATCH] MT#13201 Implement voucher API Change-Id: Ia82579179a29245d51c5ae3d3ea71054ceaf1bf0 --- lib/NGCP/Panel/Controller/API/Vouchers.pm | 198 ++++++++++++++++++ lib/NGCP/Panel/Controller/API/VouchersItem.pm | 191 +++++++++++++++++ lib/NGCP/Panel/Form/Voucher/AdminAPI.pm | 16 ++ lib/NGCP/Panel/Form/Voucher/Reseller.pm | 2 +- lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm | 69 ++++++ lib/NGCP/Panel/Role/API/Vouchers.pm | 151 +++++++++++++ ngcp_panel.conf | 7 +- t/api-root.t | 1 + t/api-vouchers.t | 186 ++++++++++++++++ 9 files changed, 819 insertions(+), 2 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/Vouchers.pm create mode 100644 lib/NGCP/Panel/Controller/API/VouchersItem.pm create mode 100644 lib/NGCP/Panel/Form/Voucher/AdminAPI.pm create mode 100644 lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm create mode 100644 lib/NGCP/Panel/Role/API/Vouchers.pm create mode 100644 t/api-vouchers.t diff --git a/lib/NGCP/Panel/Controller/API/Vouchers.pm b/lib/NGCP/Panel/Controller/API/Vouchers.pm new file mode 100644 index 0000000000..5074e085cd --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/Vouchers.pm @@ -0,0 +1,198 @@ +package NGCP::Panel::Controller::API::Vouchers; +use Sipwise::Base; +use namespace::sweep; +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +class_has 'api_description' => ( + is => 'ro', + isa => 'Str', + default => + 'Defines vouchers to top-up subscriber balances.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + { + param => 'reseller_id', + description => 'Filter for vouchers belonging to a specific reseller', + query => { + first => sub { + my $q = shift; + { reseller_id => $q }; + }, + second => sub {}, + }, + }, + ]}, +); + +with 'NGCP::Panel::Role::API::Vouchers'; + +class_has('resource_name', is => 'ro', default => 'vouchers'); +class_has('dispatch_path', is => 'ro', default => '/api/vouchers/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-vouchers'); + +__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(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->search({}, {prefetch => ['reseller']})->all) { + push @embedded, $self->hal_from_item($c, $item, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->id), + ); + } + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows)); + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows)); + } + + 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 => $allowed_methods->join(', '), + 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; + { + 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, + ); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $resource->{reseller_id} = $c->user->reseller_id; + } + + my $item; + my $code = $self->encrypt_code($c, $resource->{code}); + $item = $c->model('DB')->resultset('vouchers')->find({ + reseller_id => $resource->{reseller_id}, + code => $code, + }); + if($item) { + $c->log->error("voucher with code '$$resource{code}' already exists for reseller_id '$$resource{reseller_id}'"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Voucher with this code already exists for this reseller"); + last; + } + $resource->{code} = $code; + $resource->{created_at} = NGCP::Panel::Utils::DateTime::current_local; + + + try { + $item = $c->model('DB')->resultset('vouchers')->create($resource); + } catch($e) { + $c->log->error("failed to create voucher: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create voucher."); + last; + } + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $item->id)); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/VouchersItem.pm b/lib/NGCP/Panel/Controller/API/VouchersItem.pm new file mode 100644 index 0000000000..8ba8728c92 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/VouchersItem.pm @@ -0,0 +1,191 @@ +package NGCP::Panel::Controller::API::VouchersItem; +use Sipwise::Base; +use namespace::sweep; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ValidateJSON qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API::Vouchers'; + +class_has('resource_name', is => 'ro', default => 'vouchers'); +class_has('dispatch_path', is => 'ro', default => '/api/vouchers/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-vouchers'); + +__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(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, voucher => $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 => $allowed_methods->join(', '), + Accept_Patch => 'application/json-patch+json', + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub PATCH :Allow { + my ($self, $c, $id) = @_; + 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', + ); + last unless $json; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, voucher => $item); + my $old_resource = { $item->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $guard->commit; + + 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, $item, $form); + 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 PUT :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, voucher => $item); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $item->get_inflated_columns }; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $guard->commit; + + 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, $item, $form); + 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) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, voucher => $item); + $item->delete; + + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Voucher/AdminAPI.pm b/lib/NGCP/Panel/Form/Voucher/AdminAPI.pm new file mode 100644 index 0000000000..5153bac5df --- /dev/null +++ b/lib/NGCP/Panel/Form/Voucher/AdminAPI.pm @@ -0,0 +1,16 @@ +package NGCP::Panel::Form::Voucher::AdminAPI; + +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::Voucher::ResellerAPI'; + +has_field 'reseller' => ( + type => '+NGCP::Panel::Field::Reseller', + validate_when_empty => 1, + element_attr => { + rel => ['tooltip'], + title => ['The reseller id this voucher belongs to.'] + }, +); + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Voucher/Reseller.pm b/lib/NGCP/Panel/Form/Voucher/Reseller.pm index c5b0487495..6bf55a5926 100644 --- a/lib/NGCP/Panel/Form/Voucher/Reseller.pm +++ b/lib/NGCP/Panel/Form/Voucher/Reseller.pm @@ -18,7 +18,7 @@ has_field 'id' => ( has_field 'code' => ( type => 'Text', required => 1, - maxlength => 255, + maxlength => 128, element_attr => { rel => ['tooltip'], title => ['The voucher code.'] diff --git a/lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm b/lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm new file mode 100644 index 0000000000..edc317e4b6 --- /dev/null +++ b/lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm @@ -0,0 +1,69 @@ +package NGCP::Panel::Form::Voucher::ResellerAPI; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; +use Moose::Util::TypeConstraints; + +use HTML::FormHandler::Widget::Block::Bootstrap; + +has '+widget_wrapper' => ( default => 'Bootstrap' ); +has_field 'submitid' => ( type => 'Hidden' ); +sub build_render_list {[qw/submitid fields actions/]} +sub build_form_element_class { [qw/form-horizontal/] } + +has_field 'id' => ( + type => 'Hidden' +); + +has_field 'code' => ( + type => 'Text', + required => 1, + maxlength => 128, + element_attr => { + rel => ['tooltip'], + title => ['The voucher code.'] + }, +); + +has_field 'amount' => ( + type => 'Money', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The amount of the voucher in cents of Euro/USD/etc.'] + }, + default => '0', +); + +has_field 'valid_until' => ( + type => '+NGCP::Panel::Field::DateTime', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The date until this voucher is valid (YYYY-MM-DD hh:mm:ss).'] + }, +); + +has_field 'customer' => ( + type => '+NGCP::Panel::Field::CustomerContract', + element_attr => { + rel => ['tooltip'], + title => ['The customer contract this voucher can be used by (optional).'] + }, +); + +sub validate_valid_until { + my ($self, $field) = @_; + + unless($field->value =~ /^(\d{4})\-\d{2}\-\d{2}(T| )\d{2}:\d{2}:\d{2}$/) { + my $err_msg = 'Invalid date format, must be YYYY-MM-DD'; + $field->add_error($err_msg); + } + if(int($1) > 2037) { + my $err_msg = 'Invalid date format, YYYY must not be greater than 2037'; + $field->add_error($err_msg); + } +} + +1 +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/Vouchers.pm b/lib/NGCP/Panel/Role/API/Vouchers.pm new file mode 100644 index 0000000000..de5bfe4297 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/Vouchers.pm @@ -0,0 +1,151 @@ +package NGCP::Panel::Role::API::Vouchers; +use Moose::Role; +use Sipwise::Base; +use Crypt::Rijndael; +use MIME::Base64; +with 'NGCP::Panel::Role::API' => { + -alias =>{ item_rs => '_item_rs', }, + -excludes => [ 'item_rs' ], +}; + +use boolean qw(true); +use TryCatch; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use NGCP::Panel::Form::Voucher::AdminAPI; +use NGCP::Panel::Form::Voucher::ResellerAPI; + +sub item_rs { + my ($self, $c) = @_; + + my $item_rs = $c->model('DB')->resultset('vouchers'); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ reseller_id => $c->user->reseller_id }); + } + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + if($c->user->roles eq "admin") { + return NGCP::Panel::Form::Voucher::AdminAPI->new; + } elsif($c->user->roles eq "reseller") { + return NGCP::Panel::Form::Voucher::ResellerAPI->new; + } +} + +sub hal_from_item { + my ($self, $c, $item, $form) = @_; + my %resource = $item->get_inflated_columns; + + 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)), + 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, + ); + + $resource{valid_until} = $item->valid_until->ymd('-') . ' ' . $item->valid_until->hms(':'); + $resource{code} = $self->decrypt_code($c, $item->code); + $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); +} + +sub encrypt_code { + my ($self, $c, $plain) = @_; + + my $key = $c->config->{vouchers}->{key}; + my $iv = $c->config->{vouchers}->{iv}; + + # pkcs#5 padding to 16 bytes blocksize + my $pad = 16 - (length $plain) % 16; + $plain .= pack('C', $pad) x $pad; + + my $cipher = Crypt::Rijndael->new( + $key, + Crypt::Rijndael::MODE_CBC() + ); + $cipher->set_iv($iv); + my $crypted = $cipher->encrypt($plain); + my $b64 = encode_base64($crypted, ''); + return $b64; +} + +sub decrypt_code { + my ($self, $c, $code) = @_; + + my $key = $c->config->{vouchers}->{key}; + my $iv = $c->config->{vouchers}->{iv}; + + my $cipher = Crypt::Rijndael->new( + $key, + Crypt::Rijndael::MODE_CBC() + ); + $cipher->set_iv($iv); + my $crypted = decode_base64($code); + my $plain = $cipher->decrypt($crypted) . ""; + # remove padding + $plain =~ s/[\x01-\x1e]*$//; + return $plain; +} + +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $resource->{reseller_id} = $c->user->reseller_id; + } + + my $code = $self->encrypt_code($c, $resource->{code}); + my $dup_item = $c->model('DB')->resultset('vouchers')->find({ + reseller_id => $resource->{reseller_id}, + code => $code, + }); + if($dup_item && $dup_item->id != $item->id) { + $c->log->error("voucher with code '$$resource{code}' already exists for reseller_id '$$resource{reseller_id}'"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Voucher with this code already exists for this reseller"); + return; + } + $resource->{code} = $code; + + $item->update($resource); + + return $item; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/ngcp_panel.conf b/ngcp_panel.conf index 409aa9e9a2..5a4627f939 100644 --- a/ngcp_panel.conf +++ b/ngcp_panel.conf @@ -332,4 +332,9 @@ log4perl.appender.Default.layout.ConversionPattern=%d{ISO8601} [%p] [%F +%L] %m{ enabled 1 - \ No newline at end of file + + + + key iBmTdavJ8joPW3HO + iv tww21lQe6cmywrp3 + diff --git a/t/api-root.t b/t/api-root.t index fb06ffb6f7..25b304f6cc 100644 --- a/t/api-root.t +++ b/t/api-root.t @@ -105,6 +105,7 @@ $ua->ssl_opts( voicemailrecordings => 1, voicemails => 1, voicemailsettings => 1, + vouchers => 1, }; foreach my $link(@links) { my $rex = qr!^; rel="collection http://purl\.org/sipwise/ngcp-api/#rel-([a-z]+s)"$!; diff --git a/t/api-vouchers.t b/t/api-vouchers.t new file mode 100644 index 0000000000..af84d256e1 --- /dev/null +++ b/t/api-vouchers.t @@ -0,0 +1,186 @@ +use Sipwise::Base; +use Net::Domain qw(hostfqdn); +use LWP::UserAgent; +use JSON qw(); +use Test::More; +use Storable qw(); +use Data::Printer; + +use JSON::PP; +use LWP::Debug; + +BEGIN { + unshift(@INC,'../lib'); +} + +my $json = JSON::PP->new(); +$json->allow_blessed(1); +$json->convert_blessed(1); + +my $is_local_env = $ENV{LOCAL_TEST} // 0; +my $mysql_sqlstrict = 1; #https://bugtracker.sipwise.com/view.php?id=12565 + +use Config::General; +my $catalyst_config; +if ($is_local_env) { + my $panel_config; + for my $path(qw#../ngcp_panel.conf ngcp_panel.conf#) { + if(-f $path) { + $panel_config = $path; + last; + } + } + $panel_config //= '../ngcp_panel.conf'; + $catalyst_config = Config::General->new($panel_config); +} else { + #taken 1:1 from /lib/NGCP/Panel.pm + my $panel_config; + for my $path(qw#/etc/ngcp-panel/ngcp_panel.conf etc/ngcp_panel.conf ngcp_panel.conf#) { + if(-f $path) { + $panel_config = $path; + last; + } + } + $panel_config //= 'ngcp_panel.conf'; + $catalyst_config = Config::General->new($panel_config); +} +my %config = $catalyst_config->getall(); + +my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443'); + +my $valid_ssl_client_cert = $ENV{API_SSL_CLIENT_CERT} || + "/etc/ngcp-panel/api_ssl/NGCP-API-client-certificate.pem"; +my $valid_ssl_client_key = $ENV{API_SSL_CLIENT_KEY} || + $valid_ssl_client_cert; +my $ssl_ca_cert = $ENV{API_SSL_CA_CERT} || "/etc/ngcp-panel/api_ssl/api_ca.crt"; + +my ($ua, $req, $res); +$ua = LWP::UserAgent->new; + +if ($is_local_env) { + $ua->ssl_opts( + verify_hostname => 0, + ); + my $realm = $uri; $realm =~ s/^https?:\/\///; + $ua->credentials($realm, "api_admin_http", 'administrator', 'administrator'); + #$ua->timeout(500); #useless, need to change the nginx timeout +} else { + $ua->ssl_opts( + SSL_cert_file => $valid_ssl_client_cert, + SSL_key_file => $valid_ssl_client_key, + SSL_ca_file => $ssl_ca_cert, + ); +} + +my $t = time; +my $default_reseller_id = 1; + +test_voucher(); +done_testing(); + + +sub test_voucher { + my $code = 'testcode'.$t; + my $voucher = { + amount => 100, + code => $code, + customer_id => undef, + reseller_id => $default_reseller_id, + valid_until => '2037-01-01 12:00:00', + }; + p $voucher; + $req = HTTP::Request->new('POST', $uri.'/api/vouchers/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json($voucher)); + $res = $ua->request($req); + is($res->code, 201, _get_request_test_message("POST test voucher")); + my $voucher_uri = $uri.'/'.$res->header('Location'); + $req = HTTP::Request->new('GET', $voucher_uri); + $res = $ua->request($req); + is($res->code, 200, _get_request_test_message("fetch POSTed test voucher")); + my $post_voucher = JSON::from_json($res->decoded_content); + delete $post_voucher->{_links}; + my $voucher_id = delete $post_voucher->{id}; + is_deeply($voucher, $post_voucher, "check POSTed voucher against fetched"); + $post_voucher->{id} = $voucher_id; + + $req = HTTP::Request->new('PUT', $voucher_uri); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json($post_voucher)); + $res = $ua->request($req); + is($res->code, 200, _get_request_test_message("PUT test voucher")); + $req = HTTP::Request->new('GET', $voucher_uri); + $res = $ua->request($req); + is($res->code, 200, _get_request_test_message("fetch PUT test voucher")); + my $put_voucher = JSON::from_json($res->decoded_content); + delete $put_voucher->{_links}; + $voucher_id = delete $put_voucher->{id}; + is_deeply($voucher, $put_voucher, "check PUTed voucher against POSTed voucher"); + + p $put_voucher; + $req = HTTP::Request->new('POST', $uri.'/api/vouchers/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json($put_voucher)); + $res = $ua->request($req); + is($res->code, 422, _get_request_test_message("POST same voucher code again")); + + $put_voucher->{id} = $voucher_id; + + +# $req = HTTP::Request->new('PATCH', $billingzone_uri); +# $req->header('Content-Type' => 'application/json-patch+json'); +# $req->header('Prefer' => 'return=representation'); +# $req->content(JSON::to_json( +# [ { op => 'replace', path => '/zone', value => 'AT' } ] +# )); +# $res = $ua->request($req); +# is($res->code, 200, _get_request_test_message("PATCH test billingzone")); +# $req = HTTP::Request->new('GET', $billingzone_uri); +# $res = $ua->request($req); +# is($res->code, 200, _get_request_test_message("fetch PATCHed test billingzone")); +# $billingzone = JSON::from_json($res->decoded_content); + + + # mysql has an issue with datetime overruns,check for max date + $req = HTTP::Request->new('PATCH', $voucher_uri); + $req->header('Content-Type' => 'application/json-patch+json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json( + [ { op => 'replace', path => '/valid_until', value => '2099-01-01 00:00:00' } ] + )); + $res = $ua->request($req); + is($res->code, 422, _get_request_test_message("PATCH too far valid_until in voucher")); + + $req = HTTP::Request->new('DELETE', $voucher_uri); + $res = $ua->request($req); + is($res->code, 204, _get_request_test_message("delete POSTed test voucher")); + $req = HTTP::Request->new('GET', $voucher_uri); + $res = $ua->request($req); + is($res->code, 404, _get_request_test_message("fetch DELETEd test voucher")); +} + +sub _to_json { + return $json->encode(shift); +} + +sub _from_json { + return $json->decode(shift); +} + +sub _get_request_test_message { + my ($message) = @_; + my $code = $res->code; + if ($code == 200 || $code == 201 || $code == 204) { + return $message; + } else { + my $error_content = _from_json($res->content); + if (defined $error_content && defined $error_content->{message}) { + return $message . ' (' . $res->message . ': ' . $error_content->{message} . ')'; + } else { + return $message . ' (' . $res->message . ')'; + } + } +} + +# vim: set tabstop=4 expandtab: