MT#3997 Fix general collection/item handling.

Use paging.
Start adding tests for contacts/ collection.
Move more common functions to Role.
Use proper forms for field checking.
agranig/rest
Andreas Granig 12 years ago
parent a4a9f703bd
commit 515990623f

@ -37,6 +37,13 @@ __PACKAGE__->config(
action_roles => [qw(HTTPMethods)],
);
sub auto :Allow {
my ($self, $c) = @_;
$self->set_body($c);
$c->log->debug("++++++++++++++++ request body: " . $c->stash->{body});
}
sub GET :Allow {
my ($self, $c) = @_;
my $page = $c->request->params->{page} // 1;
@ -132,7 +139,12 @@ sub POST :Allow {
my $contact_form;
if($c->user->roles eq "api_admin") {
$contact_form = NGCP::Panel::Form::Contact::Admin->new;
if($resource->{reseller_id}) {
$contact_form = NGCP::Panel::Form::Contact::Admin->new;
} else {
$contact_form = NGCP::Panel::Form::Contact::Reseller->new;
delete $resource->{reseller_id};
}
} else {
$contact_form = NGCP::Panel::Form::Contact::Reseller->new;
$resource->{reseller_id} = $c->user->reseller_id;
@ -143,10 +155,14 @@ sub POST :Allow {
form => $contact_form,
);
my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id});
unless($reseller) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid reseller_id."); # TODO: log error, ...
last;
if($c->user->roles eq "api_admin" && !exists $resource->{reseller_id}) {
# admins can pass a contact without reseller_id
} else {
my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id});
unless($reseller) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid reseller_id."); # TODO: log error, ...
last;
}
}
my $now = DateTime->now;
@ -227,6 +243,7 @@ sub end : Private {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
$c->clear_errors;
}
$c->log->debug("++++++++++++++++ response body: " . $c->response->body // '');
}
# vim: set tabstop=4 expandtab:

@ -13,6 +13,7 @@ use HTTP::Status qw(:constants);
use JSON qw();
use MooseX::ClassAttribute qw(class_has);
use NGCP::Panel::Form::Contact::Reseller qw();
use NGCP::Panel::Form::Contact::Admin qw();
use NGCP::Panel::Utils::ValidateJSON qw();
use Path::Tiny qw(path);
use Safe::Isa qw($_isa);
@ -40,6 +41,12 @@ __PACKAGE__->config(
action_roles => [qw(HTTPMethods)],
);
sub auto :Allow {
my ($self, $c) = @_;
$self->set_body($c);
$c->log->debug("++++++++++++++++ request body: " . $c->stash->{body});
}
sub GET :Allow {
my ($self, $c, $id) = @_;
{
@ -47,7 +54,8 @@ sub GET :Allow {
last if $self->cached($c);
my $contact = $self->contact_by_id($c, $id);
last unless $self->resource_exists($c, contact => $contact);
my $hal = $self->hal_from_contact($contact);
my $hal = $self->hal_from_contact($c, $contact);
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"|;
@ -117,14 +125,14 @@ sub PATCH :Allow {
if ('*' eq $c->request->header('If-Match')) {
$contact = $self->contact_by_id($c, $id);
last unless $self->resource_exists($c, contact => $contact);
$entity = JSON::decode_json($self->hal_from_contact($contact)->as_json);
$entity = JSON::decode_json($self->hal_from_contact($c, $contact)->as_json);
} else {
$self->error($c, HTTP_PRECONDITION_FAILED, "This 'contact' entity cannot be found, it is either expired or does not exist. Fetch a fresh one.");
last;
}
}
last unless $self->require_body($c);
my $json = do { local $/; $c->request->body->getline }; # slurp
my $json = $c->stash->{body};
last unless $self->require_wellformed_json($c, $media_type, $json);
last unless $self->require_valid_patch($c, $json);
$entity = $self->apply_patch($c, $entity, $json);
@ -194,7 +202,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
$hal = $self->hal_from_contact($contact);
$hal = $self->hal_from_contact($c, $contact);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
Cache_Control => 'no-cache, private',
@ -213,17 +221,19 @@ sub PATCH :Allow {
sub PUT :Allow {
my ($self, $c, $id) = @_;
my $media_type = 'application/hal+json';
my $guard = $c->model('DB')->txn_scope_guard;
{
last unless $self->valid_id($c, $id);
last unless $self->forbid_link_header($c);
last unless $self->valid_media_type($c, $media_type);
last unless $self->require_precondition($c, 'If-Match');
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 $contact;
my $cached = $c->cache->get($c->request->uri->canonical->as_string);
my ($contact, $entity);
if ($cached) {
try {
die 'not a response object' unless $cached->$_isa('HTTP::Response');
@ -232,8 +242,15 @@ sub PUT :Allow {
};
last unless $self->valid_precondition($c, $cached->header('ETag'), 'contact');
try {
=pod
# TODO: what does this actually do? we need to go to DB for fetching the
# contact anyways, no?
NGCP::Panel::Utils::ValidateJSON->new($cached->content);
$entity = JSON::decode_json($cached->content);
$resource = JSON::decode_json($cached->content);
delete $resource->{_links};
delete $resource->{_embedded};
$c->log->debug("+++++++++++++++ got contact for PUT from cache");
=cut
} catch($e) {
die "cache poisoned: $e";
};
@ -241,67 +258,32 @@ sub PUT :Allow {
if ('*' eq $c->request->header('If-Match')) {
$contact = $self->contact_by_id($c, $id);
last unless $self->resource_exists($c, contact => $contact);
$entity = JSON::decode_json($self->hal_from_contact($contact)->as_json);
} else {
$self->error($c, HTTP_PRECONDITION_FAILED, "This 'contact' entity cannot be found, it is either expired or does not exist. Fetch a fresh one.");
$self->error($c, HTTP_PRECONDITION_FAILED, "This 'contact' entity cannot be found, it is either expired or does not exist. Fetch a fresh one.");
last;
}
}
last unless $self->require_body($c);
my $json = do { local $/; $c->request->body->getline }; # slurp
last unless $self->require_wellformed_json($c, $media_type, $json);
$entity = JSON::decode_json($json);
last unless $self->valid_entity($c, $entity);
my $hal = Data::HAL->from_json($json);
my $r_id;
{
my $reseller_link = ($hal->links // [])->grep(sub {
$_->relation->eq('http://purl.org/sipwise/ngcp-api/#rel-resellers')
});
if ($reseller_link->size) {
my $reseller_uri = URI->new_abs($reseller_link->at(0)->href->as_string, $c->req->uri)->canonical;
my $resellers_uri = URI->new_abs('/api/resellers/', $c->req->uri)->canonical;
if (0 != index $reseller_uri, $resellers_uri) {
$c->response->status(HTTP_UNPROCESSABLE_ENTITY);
$c->response->header('Content-Language' => 'en');
$c->response->content_type('application/xhtml+xml');
$c->stash(
template => 'api/unprocessable_entity.tt',
error_message => "The link $reseller_uri cannot express a reseller relationship.",
);
last;
}
$r_id = $reseller_uri->rel($resellers_uri)->query_param('id');
last unless $self->valid_id($c, $r_id);
my $contact_form;
if($c->user->roles eq "api_admin") {
if($resource->{reseller_id}) {
$contact_form = NGCP::Panel::Form::Contact::Admin->new;
} else {
$contact_form = NGCP::Panel::Form::Contact::Reseller->new;
delete $resource->{reseller_id};
}
} else {
$contact_form = NGCP::Panel::Form::Contact::Reseller->new;
# keep the reseller_id as is if a reseller is updating a contact
delete $resource->{reseller_id};
}
my $resource = $hal->resource;
my $contact_form = NGCP::Panel::Form::Contact::Reseller->new;
my %fields = map { $_->name => undef } grep { 'Text' eq $_->type || 'Email' eq $_->type } $contact_form->fields;
for my $k (keys %{ $resource }) {
delete $resource->{$k} unless exists $fields{$k};
$resource->{$k} = DateTime::Format::RFC3339->format_datetime($resource->{$k})
if $resource->{$k}->$_isa('DateTime');
}
my $result = $contact_form->run(params => $resource);
if ($result->error_results->size) {
$c->response->status(HTTP_UNPROCESSABLE_ENTITY);
$c->response->header('Content-Language' => 'en');
$c->response->content_type('application/xhtml+xml');
my $e = $result->error_results->map(sub {
sprintf '%s: %s - %s', $_->name, $_->input, $_->errors->join(q())
})->join("\n");
$c->stash(
template => 'api/unprocessable_entity.tt',
error_message => "Validation failed: $e",
);
last;
}
last unless $self->validate_form(
c => $c,
resource => $resource,
form => $contact_form,
);
$resource->{reseller_id} = $r_id;
$resource->{modify_timestamp} = DateTime->now;
$contact = $self->contact_by_id($c, $id) unless $contact;
$contact->update($resource);
@ -313,7 +295,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
$hal = $self->hal_from_contact($contact);
my $hal = $self->hal_from_contact($c, $contact);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
Cache_Control => 'no-cache, private',
@ -332,14 +314,14 @@ sub PUT :Allow {
sub contact_by_id : Private {
my ($self, $c, $id) = @_;
return $c->model('DB')->resultset('contacts')->find({'me.id' => $id}, {prefetch => ['reseller']});
return $c->model('DB')->resultset('contacts')->find({'me.id' => $id});
}
sub hal_from_contact : Private {
my ($self, $contact) = @_;
my ($self, $c, $contact) = @_;
# XXX invalid 00-00-00 dates
my %resource = $contact->get_inflated_columns;
my $id = delete $resource{id};
my $id = $resource{id};
$self->last_modified(delete $resource{modify_timestamp});
my $hal = Data::HAL->new(
@ -352,22 +334,29 @@ sub hal_from_contact : Private {
),
Data::HAL::Link->new(relation => 'collection', href => '/api/contacts/'),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
Data::HAL::Link->new(relation => 'self', href => "/api/contacts/?id=$id"),
Data::HAL::Link->new(relation => 'self', href => "/api/contacts/$id"),
$contact->reseller
? Data::HAL::Link->new(
relation => 'ngcp:resellers',
href => sprintf('/api/resellers/?id=%d', $contact->reseller_id),
href => sprintf('/api/resellers/%d', $contact->reseller_id),
) : (),
],
relation => 'ngcp:contacts',
);
my %fields = map { $_->name => undef } grep { 'Text' eq $_->type || 'Email' eq $_->type }
NGCP::Panel::Form::Contact::Reseller->new->fields;
for my $k (keys %resource) {
delete $resource{$k} unless exists $fields{$k};
$resource{$k} = DateTime::Format::RFC3339->format_datetime($resource{$k}) if $resource{$k}->$_isa('DateTime');
my $form;
if($c->user->roles eq "api_admin") {
$form = NGCP::Panel::Form::Contact::Admin->new;
} else {
$form = NGCP::Panel::Form::Contact::Reseller->new;
}
$self->validate_form(
c => $c,
resource => \%resource,
form => $form,
run => 0,
);
$hal->resource({%resource});
return $hal;
}
@ -383,4 +372,8 @@ sub end : Private {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
$c->clear_errors;
}
$c->log->debug("++++++++++++++++ response body: " . ($c->response->body // ''));
}
# vim: set tabstop=4 expandtab:

@ -41,6 +41,14 @@ __PACKAGE__->config(
action_roles => [qw(HTTPMethods)],
);
sub auto :Allow {
my ($self, $c) = @_;
$self->set_body($c);
$c->log->debug("++++++++++++++++ request body: " . $c->stash->{body});
}
sub GET :Allow :Args(0) {
my ($self, $c) = @_;
my $page = $c->request->params->{page} // 1;
@ -224,6 +232,7 @@ sub end : Private {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
$c->clear_errors;
}
$c->log->debug("++++++++++++++++ response body: " . ($c->response->body // ''));
}

@ -42,6 +42,13 @@ __PACKAGE__->config(
action_roles => [qw(HTTPMethods)],
);
sub auto :Allow {
my ($self, $c) = @_;
$self->set_body($c);
$c->log->debug("++++++++++++++++ request body: " . $c->stash->{body});
}
sub GET :Allow {
my ($self, $c, $id) = @_;
{
@ -49,7 +56,7 @@ sub GET :Allow {
last if $self->cached($c);
my $contract = $self->contract_by_id($c, $id);
last unless $self->resource_exists($c, contract => $contract);
my $hal = $self->hal_from_contract($contract);
my $hal = $self->hal_from_contract($c, $contract);
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-contacts)"|rel="item $1"|;
@ -119,14 +126,14 @@ sub PATCH :Allow {
if ('*' eq $c->request->header('If-Match')) {
$contract = $self->contract_by_id($c, $id);
last unless $self->resource_exists($c, contract => $contract);
$entity = JSON::decode_json($self->hal_from_contract($contract)->as_json);
$entity = JSON::decode_json($self->hal_from_contract($c, $contract)->as_json);
} else {
$self->error($c, HTTP_PRECONDITION_FAILED, "This 'contract' entity cannot be found, it is either expired or does not exist. Fetch a fresh one.");
last;
}
}
last unless $self->require_body($c);
my $json = do { local $/; $c->request->body->getline }; # slurp
my $json = $c->stash->{body};
last unless $self->require_wellformed_json($c, $media_type, $json);
last unless $self->require_valid_patch($c, $json);
$entity = $self->apply_patch($c, $entity, $json);
@ -181,7 +188,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
$hal = $self->hal_from_contract($contract);
$hal = $self->hal_from_contract($c, $contract);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
Cache_Control => 'no-cache, private',
@ -228,14 +235,14 @@ sub PUT :Allow {
if ('*' eq $c->request->header('If-Match')) {
$contract = $self->contract_by_id($c, $id);
last unless $self->resource_exists($c, contract => $contract);
$entity = JSON::decode_json($self->hal_from_contract($contract)->as_json);
$entity = JSON::decode_json($self->hal_from_contract($c, $contract)->as_json);
} else {
$self->error($c, HTTP_PRECONDITION_FAILED, "This 'contract' entity cannot be found, it is either expired or does not exist. Fetch a fresh one.");
last;
}
}
last unless $self->require_body($c);
my $json = do { local $/; $c->request->body->getline }; # slurp
my $json = $c->stash->{body};
last unless $self->require_wellformed_json($c, $media_type, $json);
$entity = JSON::decode_json($json);
last unless $self->valid_entity($c, $entity);
@ -285,7 +292,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
$hal = $self->hal_from_contract($contract);
$hal = $self->hal_from_contract($c, $contract);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
Cache_Control => 'no-cache, private',
@ -308,7 +315,7 @@ sub contract_by_id :Private {
}
sub hal_from_contract :Private {
my ($self, $contract) = @_;
my ($self, $c, $contract) = @_;
# XXX invalid 00-00-00 dates
my %resource = $contract->get_inflated_columns;
my $id = delete $resource{id};
@ -324,21 +331,24 @@ sub hal_from_contract :Private {
),
Data::HAL::Link->new(relation => 'collection', href => '/api/contracts/'),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
Data::HAL::Link->new(relation => 'self', href => "/api/contracts/?id=$id"),
Data::HAL::Link->new(relation => 'self', href => "/api/contracts/$id"),
$contract->contact
? Data::HAL::Link->new(
relation => 'ngcp:contacts',
href => sprintf('/api/contacts/?id=%d', $contract->contact_id),
href => sprintf('/api/contacts/%d', $contract->contact_id),
) : (),
],
relation => 'ngcp:contracts',
);
my %fields = map { $_ => undef } qw(external_id status);
for my $k (keys %resource) {
delete $resource{$k} unless exists $fields{$k};
$resource{$k} = DateTime::Format::RFC3339->format_datetime($resource{$k}) if $resource{$k}->$_isa('DateTime');
}
my $form = NGCP::Panel::Form::Contract::PeeringReseller->new;
$self->validate_form(
c => $c,
resource => \%resource,
form => $form,
run => 0,
);
$hal->resource({%resource});
return $hal;
}
@ -354,4 +364,7 @@ sub end : Private {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
$c->clear_errors;
}
$c->log->debug("++++++++++++++++ response body: " . ($c->response->body // ''));
}
# vim: set tabstop=4 expandtab:

@ -30,6 +30,13 @@ __PACKAGE__->config(
action_roles => [qw(HTTPMethods)],
);
sub auto :Allow {
my ($self, $c) = @_;
$self->set_body($c);
$c->log->debug("++++++++++++++++ request body: " . $c->stash->{body});
}
sub GET :Allow {
my ($self, $c) = @_;
$c->response->status(HTTP_NOT_IMPLEMENTED);

@ -16,6 +16,8 @@ require Catalyst::ActionRole::CheckTrailingSlash;
require Catalyst::ActionRole::HTTPMethods;
require Catalyst::ActionRole::RequireSSL;
with 'NGCP::Panel::Role::API';
class_has('dispatch_path', is => 'ro', default => '/api/');
__PACKAGE__->config(
@ -72,41 +74,6 @@ sub OPTIONS : Allow {
return;
}
sub allowed_methods : Private {
my $meta = __PACKAGE__->meta;
my @allow;
for my $method ($meta->get_method_list) {
push @allow, $meta->get_method($method)->name
if $meta->get_method($method)->can('attributes') && 'Allow' ~~ $meta->get_method($method)->attributes;
}
return [sort @allow];
}
sub cached : Private {
my ($self, $c) = @_;
my $response = $c->cache->get($c->request->uri->canonical->as_string);
return unless $response;
my $matched_tag = $c->request->header('If-None-Match')
&& ('*' eq $c->request->header('If-None-Match'))
|| (
grep { $response->header('ETag') eq $_ }
Data::Record->new({ split => qr/\s*,\s*/, unless => $RE{quoted}, })
->records($c->request->header('If-None-Match'))
);
my $not_modified = $c->request->header('If-Modified-Since')
&& !($self->last_modified < DateTime::Format::HTTP->parse_datetime($c->request->header('If-Modified-Since')));
if (
$matched_tag && $not_modified
|| $matched_tag
|| $not_modified
) {
$response->code(HTTP_NOT_MODIFIED);
$response->content(undef);
return $response;
}
return;
}
sub collections_link_headers : Private {
my ($self) = @_;
return (
@ -117,16 +84,6 @@ sub collections_link_headers : Private {
);
}
sub etag : Private {
my ($self, $octets) = @_;
return sprintf '"ni:/sha3-256;%s"', sha3_256_base64($octets);
}
sub expires : Private {
my ($self) = @_;
return DateTime->now->clone->add(years => 1); # XXX insert install timestamp + 1000 days/ product end-of-life
}
sub invalid_user : Private {
my ($self, $c, $ssl_client_m_serial) = @_;
$c->response->status(403);
@ -139,3 +96,9 @@ sub last_modified : Private {
my ($self, $octets) = @_;
return DateTime->new(year => 2013, month => 11, day => 11); # XXX insert release timestamp
}
sub end : Private {
my ($self, $c) = @_;
$c->log->debug("+++++++++++++++++++ API::Root::end");
}

@ -25,7 +25,25 @@ sub get_valid_post_data {
return unless $self->forbid_link_header($c);
return unless $self->valid_media_type($c, $media_type);
return unless $self->require_body($c);
my $json = do { local $/; $c->request->body->getline }; # slurp
my $json = $c->stash->{body};
return unless $self->require_wellformed_json($c, $media_type, $json);
return JSON::from_json($json);
}
sub get_valid_put_data {
my ($self, %params) = @_;
my $c = $params{c};
my $media_type = $params{media_type};
my $id = $params{id};
return unless $self->valid_id($c, $id);
return unless $self->forbid_link_header($c);
return unless $self->valid_media_type($c, $media_type);
return unless $self->require_precondition($c, 'If-Match');
return unless $self->require_body($c);
my $json = $c->stash->{body};
return unless $self->require_wellformed_json($c, $media_type, $json);
return JSON::from_json($json);
@ -37,43 +55,53 @@ sub validate_form {
my $c = $params{c};
my $resource = $params{resource};
my $form = $params{form};
my $run = $params{run} // 1;
my @normalized = ();
# move {xxx_id} into {xxx}{id} for FormHandler
foreach my $key(keys %{ $resource } ) {
if($key =~ /^([a-z]+)_id$/) {
$c->log->debug("++++++++++++ moving key $key with value " . ($resource->{$key} // '<null>') . " to $1/id");
push @normalized, $1;
$resource->{$1}{id} = delete $resource->{$key};
}
}
use Data::Printer; p $resource;
# remove unknown keys
my %fields = map { $_->name => undef } $form->fields;
for my $k (keys %{ $resource }) {
unless(exists $fields{$k}) {
$c->log->info("deleting unknown key '$k' from message"); # TODO: user, message trace, ...
$c->log->info("+++++++++ deleting unknown key '$k' from message"); # TODO: user, message trace, ...
delete $resource->{$k};
}
$resource->{$k} = DateTime::Format::RFC3339->format_datetime($resource->{$k})
if $resource->{$k}->$_isa('DateTime');
$resource->{$k} = $resource->{$k} + 0
if($form->field($k)->$_isa('HTML::FormHandler::Field::Integer') ||
$form->field($k)->$_isa('HTML::FormHandler::Field::Float'));
}
# check keys/vals
my $result = $form->run(params => $resource);
if ($result->error_results->size) {
my $e = $result->error_results->map(sub {
sprintf 'field=\'%s\', input=\'%s\', errors=\'%s\'', $_->name, $_->input // '', $_->errors->join(q())
})->join("; ");
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Validation failed. $e");
return;
if($run) {
# check keys/vals
my $result = $form->run(params => $resource);
if ($result->error_results->size) {
my $f = $result->error_results->[0];
use Data::Printer; p $f;
my $e = $result->error_results->map(sub {
sprintf 'field=\'%s\', input=\'%s\', errors=\'%s\'', ($_->parent->$_isa('HTML::FormHandler::Field::Result') ? $_->parent->name . '_' : '') . $_->name, $_->input // '', $_->errors->join(q())
})->join("; ");
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Validation failed. $e");
return;
}
}
# move {xxx}{id} back into {xxx_id} for DB
foreach my $key(@normalized) {
$resource->{$key . '_id'} = $resource->{$key}{id};
$resource->{$key . '_id'} = defined($resource->{$key}{id}) ?
int($resource->{$key}{id}) :
$resource->{$key}{id};
delete $resource->{$key};
}
@ -107,7 +135,7 @@ sub valid_media_type {
sub require_body {
my ($self, $c) = @_;
return 1 if $c->request->body;
return 1 if length $c->stash->{body};
$self->error($c, HTTP_BAD_REQUEST, "This request is missing a message body.");
return;
}
@ -119,6 +147,16 @@ sub require_precondition {
return;
}
sub valid_precondition {
my ($self, $c, $etag, $entity_name) = @_;
my $if_match = $c->request->header('If-Match');
return 1 if '*' eq $if_match || grep {$etag eq $_} Data::Record->new({
split => qr/\s*,\s*/, unless => $RE{delimited}{-delim => q(")},
})->records($if_match);
$self->error($c, HTTP_PRECONDITION_FAILED, "This '$entity_name' entity cannot be found, it is either expired or does not exist. Fetch a fresh one.");
return;
}
sub require_preference {
my ($self, $c) = @_;
my @preference = grep { 'return' eq $_->[0] } split_header_words($c->request->header('Prefer'));
@ -235,5 +273,10 @@ sub apply_patch {
return $entity;
}
sub set_body {
my ($self, $c) = @_;
$c->stash->{body} = $c->request->body ? (do { local $/; $c->request->body->getline }) : '';
}
1;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,159 @@
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/contacts/');
$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-contacts", "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;
{
# create 6 new system contacts (no reseller)
my %contacts = ();
for(my $i = 1; $i <= 6; ++$i) {
$req = HTTP::Request->new('POST', $uri.'/api/contacts/');
$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 => 1,
}));
$res = $ua->request($req);
ok($res->code == 201, "create test contact $i");
$contacts{$res->header('Location')} = 1;
$firstcontact = $res->header('Location') unless $firstcontact;
}
# try to create invalid contact without email
$req = HTTP::Request->new('POST', $uri.'/api/contacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "Test_First_invalid",
lastname => "Test_Last_invalid",
}));
$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 with invalid reseller_id
$req = HTTP::Request->new('POST', $uri.'/api/contacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "Test_First_invalid",
lastname => "Test_Last_invalid",
email => "test.invalid\@test.invalid",
reseller_id => 99999,
}));
$res = $ua->request($req);
ok($res->code == 422, "create test contact with invalid reseller_id");
my $reseller_err = JSON::from_json($res->decoded_content);
ok($reseller_err->{code} eq "422", "check error code in body");
ok($reseller_err->{message} =~ /Invalid reseller_id/, "check error message in body");
# iterate over contacts collection to check next/prev links
my $nexturi = $uri.'/api/contacts/?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:contacts'} eq "ARRAY" ||
ref $collection->{_links}->{'ngcp:contacts'} 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:contacts'} eq "HASH") {
# TODO: handle hashref
delete $contacts{$collection->{_links}->{'ngcp:contacts'}->{href}};
} else {
foreach my $c(@{ $collection->{_links}->{'ngcp:contacts'} }) {
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('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->{reseller_id}, "check existence of reseller_id");
if(defined $contact->{reseller_id}) {
ok($res->decoded_content =~ /\"reseller_id\"\s*:\s*\d+/, "check if reseller_id is number");
}
# PUT
}
done_testing;
# vim: set tabstop=4 expandtab:
Loading…
Cancel
Save