diff --git a/lib/NGCP/Panel/Controller/API/CustomerContacts.pm b/lib/NGCP/Panel/Controller/API/CustomerContacts.pm new file mode 100644 index 0000000000..795aa2c617 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/CustomerContacts.pm @@ -0,0 +1,176 @@ +package NGCP::Panel::Controller::API::CustomerContacts; +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::Form::Contact::Admin qw(); +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; + +with 'NGCP::Panel::Role::API'; +with 'NGCP::Panel::Role::API::CustomerContacts'; + +class_has('resource_name', is => 'ro', default => 'customercontacts'); +class_has('dispatch_path', is => 'ro', default => '/api/customercontacts/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-customercontacts'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => 'api_admin', + 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 $contacts = $c->model('DB')->resultset('contacts') + ->search({ reseller_id => { '-not' => undef } }); + my $total_count = int($contacts->count); + $contacts = $contacts->search(undef, { + page => $page, + rows => $rows, + }); + my (@embedded, @links); + my $form = NGCP::Panel::Form::Contact::Admin->new; + for my $contact ($contacts->search({}, {order_by => {-asc => 'me.id'}, prefetch => ['reseller']})->all) { + push @embedded, $self->hal_from_contact($c, $contact, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $contact->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 $rname = $self->resource_name; + 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-$rname)"|rel="item $1"|; + s/rel=self/rel="collection 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) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods; + $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 $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + + my $form = NGCP::Panel::Form::Contact::Admin->new; + $resource->{reseller_id} //= undef; + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + ); + + my $reseller = $c->model('DB')->resultset('resellers') + ->find($resource->{reseller_id}); + unless($reseller) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id'"); + last; + } + + my $now = NGCP::Panel::Utils::DateTime::current_local; + $resource->{create_timestamp} = $now; + $resource->{modify_timestamp} = $now; + my $contact; + try { + $contact = $c->model('DB')->resultset('contacts')->create($resource); + } catch($e) { + $c->log->error("failed to create contact: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create contact."); + last; + } + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $contact->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/CustomerContactsItem.pm b/lib/NGCP/Panel/Controller/API/CustomerContactsItem.pm new file mode 100644 index 0000000000..9203d89b19 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/CustomerContactsItem.pm @@ -0,0 +1,201 @@ +package NGCP::Panel::Controller::API::CustomerContactsItem; +use Sipwise::Base; +use namespace::sweep; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Form::Contact::Admin qw(); +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'; +with 'NGCP::Panel::Role::API::CustomerContacts'; + +class_has('resource_name', is => 'ro', default => 'customercontacts'); +class_has('dispatch_path', is => 'ro', default => '/api/customercontacts/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-customercontacts'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => 'api_admin', + 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 $contact = $self->contact_by_id($c, $id); + last unless $self->resource_exists($c, customercontact => $contact); + + my $hal = $self->hal_from_contact($c, $contact); + + # TODO: we don't need reseller stuff here! + 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; + $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 $contact = $self->contact_by_id($c, $id); + last unless $self->resource_exists($c, customercontact => $contact); + my $old_resource = { $contact->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = NGCP::Panel::Form::Contact::Admin->new; + $contact = $self->update_contact($c, $contact, $old_resource, $resource, $form); + last unless $contact; + + $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_contact($c, $contact, $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 $contact = $self->contact_by_id($c, $id); + last unless $self->resource_exists($c, customercontact => $contact); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $contact->get_inflated_columns }; + + my $form = NGCP::Panel::Form::Contact::Admin->new; + $contact = $self->update_contact($c, $contact, $old_resource, $resource, $form); + last unless $contact; + + $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_contact($c, $contact, $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 $contact = $self->contact_by_id($c, $id); + last unless $self->resource_exists($c, customercontact => $contact); + my $contract_count = $c->model('DB')->resultset('contracts')->search({ + contact_id => $id, + status => { '!=' => 'terminated' }, + }); + if($contract_count > 0) { + $self->error($c, HTTP_LOCKED, "Contact is still in use."); + last; + } else { + $contact->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/Controller/API/SystemContactsItem.pm b/lib/NGCP/Panel/Controller/API/SystemContactsItem.pm index f317686fe6..d733af93ec 100644 --- a/lib/NGCP/Panel/Controller/API/SystemContactsItem.pm +++ b/lib/NGCP/Panel/Controller/API/SystemContactsItem.pm @@ -91,6 +91,7 @@ sub PATCH :Allow { { my $preference = $self->require_preference($c); last unless $preference; + my $json = $self->get_valid_patch_data( c => $c, id => $id, @@ -100,20 +101,14 @@ sub PATCH :Allow { my $contact = $self->contact_by_id($c, $id); last unless $self->resource_exists($c, systemcontact => $contact); - my $resource = { $contact->get_inflated_columns }; - $resource = $self->apply_patch($c, $resource, $json); + my $old_resource = { $contact->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); last unless $resource; my $form = NGCP::Panel::Form::Contact::Reseller->new; - last unless $self->validate_form( - c => $c, - form => $form, - resource => $resource - ); - - my $now = NGCP::Panel::Utils::DateTime::current_local; - $resource->{modify_timestamp} = $now; - $contact->update($resource); + $contact = $self->update_contact($c, $contact, $old_resource, $resource, $form); + last unless $contact; + $guard->commit; if ('minimal' eq $preference) { @@ -137,27 +132,24 @@ 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 $contact = $self->contact_by_id($c, $id); + last unless $self->resource_exists($c, systemcontact => $contact); my $resource = $self->get_valid_put_data( c => $c, id => $id, media_type => 'application/json', ); last unless $resource; - my $preference = $self->require_preference($c); - last unless $preference; + my $old_resource = { $contact->get_inflated_columns }; - my $form = NGCP::Panel::Form::Contact::Reseller->new; - last unless $self->validate_form( - c => $c, - resource => $resource, - form => $form, - ); + my $form = NGCP::Panel::Form::Contact::Admin->new; + $contact = $self->update_contact($c, $contact, $old_resource, $resource, $form); + last unless $contact; - my $now = NGCP::Panel::Utils::DateTime::current_local; - $resource->{modify_timestamp} = $now; - my $contact = $self->contact_by_id($c, $id); - $contact->update($resource); - $guard->commit; + $guard->commit; if ('minimal' eq $preference) { $c->response->status(HTTP_NO_CONTENT); diff --git a/lib/NGCP/Panel/Role/API/CustomerContacts.pm b/lib/NGCP/Panel/Role/API/CustomerContacts.pm new file mode 100644 index 0000000000..9fb0bd1a9c --- /dev/null +++ b/lib/NGCP/Panel/Role/API/CustomerContacts.pm @@ -0,0 +1,82 @@ +package NGCP::Panel::Role::API::CustomerContacts; +use Moose::Role; +use Sipwise::Base; + +use boolean qw(true); +use Try::Tiny; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use NGCP::Panel::Form::Contact::Admin; + +sub hal_from_contact { + my ($self, $c, $contact, $form) = @_; + my %resource = $contact->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, $contact->id)), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= NGCP::Panel::Form::Contact::Admin->new; + $self->validate_form( + c => $c, + resource => \%resource, + form => $form, + run => 0, + ); + + $resource{id} = int($contact->id); + $hal->resource({%resource}); + return $hal; +} + +sub contact_by_id { + my ($self, $c, $id) = @_; + + # we only return system contacts, that is, a contact without reseller + my $contact_rs = $c->model('DB')->resultset('contacts') + ->search({ reseller_id => {'-not' => undef } }); + return $contact_rs->find($id); +} + +sub update_contact { + my ($self, $c, $contact, $old_resource, $resource, $form) = @_; + + $form //= NGCP::Panel::Form::Contact::Admin->new; + # TODO: for some reason, formhandler lets missing reseller_id slip thru + $resource->{reseller_id} //= undef; + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + + my $now = NGCP::Panel::Utils::DateTime::current_local; + $resource->{modify_timestamp} = $now; + + if($old_resource->{reseller_id} != $resource->{reseller_id}) { + my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id}); + unless($reseller) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id'"); + return; + } + } + + $contact->update($resource); + + return $contact; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/SystemContacts.pm b/lib/NGCP/Panel/Role/API/SystemContacts.pm index 225933d98f..c79132f24c 100644 --- a/lib/NGCP/Panel/Role/API/SystemContacts.pm +++ b/lib/NGCP/Panel/Role/API/SystemContacts.pm @@ -13,6 +13,7 @@ sub hal_from_contact { my ($self, $c, $contact, $form) = @_; my %resource = $contact->get_inflated_columns; + my $hal = Data::HAL->new( links => [ Data::HAL::Link->new( @@ -29,6 +30,9 @@ sub hal_from_contact { ); $form //= NGCP::Panel::Form::Contact::Reseller->new; + + # TODO: i'd expect reseller to be removed automatically + delete $resource{reseller_id}; $self->validate_form( c => $c, resource => \%resource, @@ -50,5 +54,24 @@ sub contact_by_id { return $contact_rs->find($id); } +sub update_contact { + my ($self, $c, $contact, $old_resource, $resource, $form) = @_; + + $form //= NGCP::Panel::Form::Contact::Reseller->new; + delete $resource->{reseller_id}; + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + + my $now = NGCP::Panel::Utils::DateTime::current_local; + $resource->{modify_timestamp} = $now; + + $contact->update($resource); + + return $contact; +} + 1; # vim: set tabstop=4 expandtab: diff --git a/t/api-customercontacts.t b/t/api-customercontacts.t new file mode 100644 index 0000000000..aa74619f65 --- /dev/null +++ b/t/api-customercontacts.t @@ -0,0 +1,286 @@ +use Sipwise::Base; +use Net::Domain qw(hostfqdn); +use LWP::UserAgent; +use JSON qw(); +use Test::More; + +my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443'); + +my $valid_ssl_client_cert = $ENV{API_SSL_CLIENT_CERT} || + "/etc/ssl/ngcp/api/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/ssl/ngcp/api/ca-cert.pem"; + +my ($ua, $req, $res); +$ua = LWP::UserAgent->new; + +$ua->ssl_opts( + SSL_cert_file => $valid_ssl_client_cert, + SSL_key_file => $valid_ssl_client_key, + SSL_ca_file => $ssl_ca_cert, +); + +# OPTIONS tests +{ + $req = HTTP::Request->new('OPTIONS', $uri.'/api/customercontacts/'); + $res = $ua->request($req); + ok($res->code == 200, "check options request"); + ok($res->header('Accept-Post') eq "application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-customercontacts", "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"); + } +} + +# collection test +my $firstcontact = undef; +my @allcontacts = (); +{ + $req = HTTP::Request->new('GET', $uri.'/api/resellers/'); + $res = $ua->request($req); + ok($res->code == 200, "fetch resellers"); + my $reseller = JSON::from_json($res->decoded_content); + if(ref $reseller->{_embedded}->{'ngcp:resellers'} eq 'ARRAY') { + $reseller = $reseller->{_embedded}->{'ngcp:resellers'}->[0]->{id}; + } elsif(ref $reseller->{_embedded}->{'ngcp:resellers'} eq 'HASH') { + $reseller = $reseller->{_embedded}->{'ngcp:resellers'}->{href}; + } else { + # TODO: hm, no resellers, we should create one + ok(0 == 1, "check if we found a reseller"); + } + + # create 6 new customer contacts + my %contacts = (); + for(my $i = 1; $i <= 6; ++$i) { + $req = HTTP::Request->new('POST', $uri.'/api/customercontacts/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + firstname => "Test_First_$i", + lastname => "Test_Last_$i", + email => "test.$i\@test.invalid", + reseller_id => $reseller, + })); + $res = $ua->request($req); + ok($res->code == 201, "create test contact $i"); + $contacts{$res->header('Location')} = 1; + push @allcontacts, $res->header('Location'); + $firstcontact = $res->header('Location') unless $firstcontact; + } + + # try to create invalid contact without email + $req = HTTP::Request->new('POST', $uri.'/api/customercontacts/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + firstname => "Test_First_invalid", + lastname => "Test_Last_invalid", + reseller_id => $reseller, + })); + $res = $ua->request($req); + ok($res->code == 422, "create invalid test contact with missing email"); + my $email_err = JSON::from_json($res->decoded_content); + ok($email_err->{code} eq "422", "check error code in body"); + ok($email_err->{message} =~ /field=\'email\'/, "check error message in body"); + # try to create invalid contact without reseller_id + $req->content(JSON::to_json({ + firstname => "Test_First_invalid", + lastname => "Test_Last_invalid", + email => "test.999\@test.invalid", + #reseller_id => $reseller, + })); + $res = $ua->request($req); + ok($res->code == 422, "create invalid test contact with missing reseller_id"); + $email_err = JSON::from_json($res->decoded_content); + ok($email_err->{code} eq "422", "check error code in body"); + ok($email_err->{message} =~ /field=\'reseller_id\'/, "check error message in body"); + + # try to create invalid contact with invalid reseller_id + $req->content(JSON::to_json({ + firstname => "Test_First_invalid", + lastname => "Test_Last_invalid", + email => "test.999\@test.invalid", + reseller_id => 99999, + })); + $res = $ua->request($req); + ok($res->code == 422, "create invalid test contact with invalid reseller_id"); + $email_err = JSON::from_json($res->decoded_content); + ok($email_err->{code} eq "422", "check error code in body"); + ok($email_err->{message} =~ /Invalid \'reseller_id\'/, "check error message in body"); + + # iterate over contacts collection to check next/prev links + my $nexturi = $uri.'/api/customercontacts/?page=1&rows=5'; + do { + $res = $ua->get($nexturi); + ok($res->code == 200, "fetch contacts page"); + my $collection = JSON::from_json($res->decoded_content); + my $selfuri = $uri . $collection->{_links}->{self}->{href}; + ok($selfuri eq $nexturi, "check _links.self.href of collection"); + my $colluri = URI->new($selfuri); + + ok($collection->{total_count} > 0, "check 'total_count' of collection"); + + my %q = $colluri->query_form; + ok(exists $q{page} && exists $q{rows}, "check existence of 'page' and 'row' in 'self'"); + my $page = int($q{page}); + my $rows = int($q{rows}); + if($page == 1) { + ok(!exists $collection->{_links}->{prev}->{href}, "check absence of 'prev' on first page"); + } else { + ok(exists $collection->{_links}->{prev}->{href}, "check existence of 'prev'"); + } + if(($collection->{total_count} / $rows) <= $page) { + ok(!exists $collection->{_links}->{next}->{href}, "check absence of 'next' on last page"); + } else { + ok(exists $collection->{_links}->{next}->{href}, "check existence of 'next'"); + } + + if($collection->{_links}->{next}->{href}) { + $nexturi = $uri . $collection->{_links}->{next}->{href}; + } else { + $nexturi = undef; + } + + # TODO: I'd expect that to be an array ref in any case! + ok((ref $collection->{_links}->{'ngcp:customercontacts'} eq "ARRAY" || + ref $collection->{_links}->{'ngcp:customercontacts'} eq "HASH"), "check if 'ngcp:contacts' is array/hash-ref"); + + # remove any contact we find in the collection for later check + if(ref $collection->{_links}->{'ngcp:customercontacts'} eq "HASH") { + # TODO: handle hashref + delete $contacts{$collection->{_links}->{'ngcp:customercontacts'}->{href}}; + } else { + foreach my $c(@{ $collection->{_links}->{'ngcp:customercontacts'} }) { + delete $contacts{$c->{href}}; + } + } + } while($nexturi); + + ok(keys %contacts == 0, "check if all test contacts have been found"); +} + +# test contacts item +{ + $req = HTTP::Request->new('OPTIONS', $uri.'/'.$firstcontact); + $res = $ua->request($req); + ok($res->code == 200, "check options on item"); + my @hopts = split /\s*,\s*/, $res->header('Allow'); + my $opts = JSON::from_json($res->decoded_content); + ok(exists $opts->{methods} && ref $opts->{methods} eq "ARRAY", "check for valid 'methods' in body"); + foreach my $opt(qw( GET HEAD OPTIONS PUT PATCH DELETE )) { + ok(grep(/^$opt$/, @hopts), "check for existence of '$opt' in Allow header"); + ok(grep(/^$opt$/, @{ $opts->{methods} }), "check for existence of '$opt' in body"); + } + my $opt = 'POST'; + ok(!grep(/^$opt$/, @hopts), "check for absence of '$opt' in Allow header"); + ok(!grep(/^$opt$/, @{ $opts->{methods} }), "check for absence of '$opt' in body"); + + $req = HTTP::Request->new('GET', $uri.'/'.$firstcontact); + $res = $ua->request($req); + ok($res->code == 200, "fetch one contact item"); + my $contact = JSON::from_json($res->decoded_content); + ok(exists $contact->{firstname}, "check existence of firstname"); + ok(exists $contact->{lastname}, "check existence of lastname"); + ok(exists $contact->{email}, "check existence of email"); + ok(exists $contact->{id} && $contact->{id}->is_int, "check existence of id"); + ok(exists $contact->{reseller_id} && $contact->{reseller_id}->is_int, "check existence of reseller_id"); + + # PUT same result again + my $old_contact = { %$contact }; + delete $contact->{_links}; + delete $contact->{_embedded}; + $req = HTTP::Request->new('PUT', $uri.'/'.$firstcontact); + $req->header('Prefer' => 'return=minimal'); + + # check if it fails without content type + $req->remove_header('Content-Type'); + $res = $ua->request($req); + ok($res->code == 415, "check put missing content type"); + + # check if it fails with unsupported content type + $req->header('Content-Type' => 'application/xxx'); + $res = $ua->request($req); + ok($res->code == 415, "check put invalid content type"); + + $req->remove_header('Content-Type'); + $req->header('Content-Type' => 'application/json'); + + # check if it fails with missing Prefer + $req->remove_header('Prefer'); + $res = $ua->request($req); + ok($res->code == 400, "check put missing prefer"); + + # check if it fails with invalid Prefer + $req->header('Prefer' => "return=invalid"); + $res = $ua->request($req); + ok($res->code == 400, "check put invalid prefer"); + + + $req->remove_header('Prefer'); + $req->header('Prefer' => "return=representation"); + + # check if it fails with missing body + $res = $ua->request($req); + ok($res->code == 400, "check put no body"); + + # check if put is ok + $req->content(JSON::to_json($contact)); + $res = $ua->request($req); + ok($res->code == 200, "check put successful"); + + my $new_contact = JSON::from_json($res->decoded_content); + is_deeply($old_contact, $new_contact, "check put if unmodified put returns the same"); + + $req = HTTP::Request->new('PATCH', $uri.'/'.$firstcontact); + $req->header('Prefer' => 'return=representation'); + $req->header('Content-Type' => 'application/json-patch+json'); + + $req->content(JSON::to_json( + [ { op => 'replace', path => '/firstname', value => 'patchedfirst' } ] + )); + $res = $ua->request($req); + ok($res->code == 200, "check patched contact item"); + my $mod_contact = JSON::from_json($res->decoded_content); + ok($mod_contact->{firstname} eq "patchedfirst", "check patched replace op"); + + $req->content(JSON::to_json( + [ { op => 'replace', path => '/firstname', value => undef } ] + )); + $res = $ua->request($req); + ok($res->code == 200, "check patched contact item"); + $mod_contact = JSON::from_json($res->decoded_content); + ok(exists $mod_contact->{firstname} && !defined $mod_contact->{firstname}, "check patched replace op for undef"); + + $req->content(JSON::to_json( + [ { op => 'replace', path => '/email', value => undef } ] + )); + $res = $ua->request($req); + ok($res->code == 422, "check patched contact with unset email"); + + $req->content(JSON::to_json( + [ { op => 'replace', path => '/reseller_id', value => undef } ] + )); + $res = $ua->request($req); + ok($res->code == 422, "check patched contact with unset reseller_id"); + + $req->content(JSON::to_json( + [ { op => 'replace', path => '/reseller_id', value => 99999 } ] + )); + $res = $ua->request($req); + ok($res->code == 422, "check patched contact with invalid reseller_id"); +} + +# DELETE +{ + foreach my $contact(@allcontacts) { + $req = HTTP::Request->new('DELETE', $uri.'/'.$contact); + $res = $ua->request($req); + ok($res->code == 204, "check delete of contact"); + } +} + +done_testing; + +# vim: set tabstop=4 expandtab: diff --git a/t/api-systemcontacts.t b/t/api-systemcontacts.t index 732c360b9c..41e496aa8b 100644 --- a/t/api-systemcontacts.t +++ b/t/api-systemcontacts.t @@ -153,6 +153,7 @@ my @allcontacts = (); delete $contact->{_links}; delete $contact->{_embedded}; $req = HTTP::Request->new('PUT', $uri.'/'.$firstcontact); + $req->header('Prefer' => 'return=minimal'); # check if it fails without content type $req->remove_header('Content-Type'); @@ -168,6 +169,7 @@ my @allcontacts = (); $req->header('Content-Type' => 'application/json'); # check if it fails with missing Prefer + $req->remove_header('Prefer'); $res = $ua->request($req); ok($res->code == 400, "check put missing prefer");