MT#11917 Journaling for first set of resources

the journal module introduces a change history of
resources modified by api invocations. the history of
the 'customer' resource demo is accessible at
/api/customers/x/journal.

Change-Id: I4d5d11bc3e35160feed587ce4c1db565991866b2
changes/24/1524/1
Rene Krenn 10 years ago
parent bcd2de62cf
commit 96c731a144

@ -186,6 +186,12 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create billing profile.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_billing_profile = $self->profile_by_id($c, $billing_profile->id);
return $self->hal_from_profile($c, $_billing_profile,$form); });
$guard->commit;

@ -21,14 +21,19 @@ class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -106,6 +111,9 @@ sub PATCH :Allow {
my $form = $self->get_form($c);
$profile = $self->update_profile($c, $profile, $old_resource, $resource, $form);
last unless $profile;
my $hal = $self->hal_from_profile($c, $profile, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -114,7 +122,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_profile($c, $profile, $form);
#my $hal = $self->hal_from_profile($c, $profile, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -147,6 +155,9 @@ sub PUT :Allow {
$profile = $self->update_profile($c, $profile, $old_resource, $resource, $form);
last unless $profile;
my $hal = $self->hal_from_profile($c, $profile, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -154,7 +165,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_profile($c, $profile, $form);
#my $hal = $self->hal_from_profile($c, $profile, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -168,6 +179,41 @@ sub PUT :Allow {
# we don't allow to DELETE a billing profile
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -22,16 +22,23 @@ class_has('resource_name', is => 'ro', default => 'callforwards');
class_has('dispatch_path', is => 'ro', default => '/api/callforwards/');
class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-callforwards');
class_has(@{ __PACKAGE__->get_journal_query_params() });
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods },
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -109,6 +116,9 @@ sub PATCH :Allow {
my $form = $self->get_form($c);
$callforward = $self->update_item($c, $callforward, $old_resource, $resource, $form);
last unless $callforward;
my $hal = $self->hal_from_item($c, $callforward, "callforwards");
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -117,7 +127,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_item($c, $callforward, "callforwards");
#my $hal = $self->hal_from_item($c, $callforward, "callforwards");
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -150,6 +160,9 @@ sub PUT :Allow {
$callforward = $self->update_item($c, $callforward, $old_resource, $resource, $form);
last unless $callforward;
my $hal = $self->hal_from_item($c, $callforward, "callforwards");
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -157,7 +170,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_item($c, $callforward, "callforwards");
#my $hal = $self->hal_from_item($c, $callforward, "callforwards");
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -190,6 +203,13 @@ sub DELETE :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
last;
}
last unless $self->add_delete_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
#my $_callforward = $self->item_by_id($c, $id, "callforwards");
return $self->hal_from_item($c,$callforward); });
$guard->commit;
$c->response->status(HTTP_NO_CONTENT);

@ -230,6 +230,12 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create contract.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_contract = $self->contract_by_id($c, $contract->id);
return $self->hal_from_contract($c,$_contract,$form); });
$guard->commit;

@ -24,14 +24,19 @@ class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => 'admin',
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods },
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => 'admin',
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -112,6 +117,9 @@ sub PATCH :Allow {
$contract = $self->update_contract($c, $contract, $old_resource, $resource, $form);
last unless $contract;
my $hal = $self->hal_from_contract($c, $contract, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -119,7 +127,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_contract($c, $contract, $form);
#my $hal = $self->hal_from_contract($c, $contract, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -153,6 +161,9 @@ sub PUT :Allow {
my $form = $self->get_form($c);
$contract = $self->update_contract($c, $contract, $old_resource, $resource, $form);
last unless $contract;
my $hal = $self->hal_from_contract($c, $contract, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -161,7 +172,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_contract($c, $contract, $form);
#my $hal = $self->hal_from_contract($c, $contract, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -203,6 +214,41 @@ sub DELETE :Allow {
=cut
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -149,7 +149,7 @@ sub OPTIONS :Allow {
sub POST :Allow {
my ($self, $c) = @_;
my $guard = $c->model('DB')->txn_scope_guard;
{
my $resource = $self->get_valid_post_data(
c => $c,
@ -193,6 +193,14 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create contact.");
last;
}
last unless $self->add_delete_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_contact = $self->contact_by_id($c, $contact->id);
return $self->hal_from_contact($c, $_contact, $form); });
$guard->commit;
$c->response->status(HTTP_CREATED);
$c->response->header(Location => sprintf('/%s%d', $c->request->path, $contact->id));

@ -21,14 +21,19 @@ class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods },
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -106,6 +111,9 @@ sub PATCH :Allow {
my $form = $self->get_form($c);
$contact = $self->update_contact($c, $contact, $old_resource, $resource, $form);
last unless $contact;
my $hal = $self->hal_from_contact($c, $contact, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -114,7 +122,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_contact($c, $contact, $form);
#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);
@ -147,6 +155,9 @@ sub PUT :Allow {
$contact = $self->update_contact($c, $contact, $old_resource, $resource, $form);
last unless $contact;
my $hal = $self->hal_from_contact($c, $contact, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -154,7 +165,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_contact($c, $contact, $form);
#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);
@ -180,6 +191,13 @@ sub DELETE :Allow {
$self->error($c, HTTP_LOCKED, "Contact is still in use.");
last;
} else {
last unless $self->add_delete_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_form = $self->get_form($c);
return $self->hal_from_contact($c, $contact, $_form); });
$contact->delete;
}
$guard->commit;
@ -190,6 +208,41 @@ sub DELETE :Allow {
return;
}
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -243,13 +243,6 @@ sub POST :Allow {
}
try {
$customer = $schema->resultset('contracts')->create($resource);
$schema->resultset('journals')->create({
type => "create",
resource => "customers",
resource_id => $customer->id,
timestamp => $now->hires_epoch,
content => undef,
});
} catch($e) {
$c->log->error("failed to create customer contract: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer.");
@ -300,6 +293,12 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_customer = $self->customer_by_id($c, $customer->id);
return $self->hal_from_customer($c,$_customer,$form); });
$guard->commit;

@ -22,16 +22,23 @@ class_has('resource_name', is => 'ro', default => 'customers');
class_has('dispatch_path', is => 'ro', default => '/api/customers/');
class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-customers');
class_has(@{ __PACKAGE__->get_journal_query_params() });
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -112,6 +119,9 @@ sub PATCH :Allow {
my $form = $self->get_form($c);
$customer = $self->update_customer($c, $customer, $old_resource, $resource, $form);
last unless $customer;
my $hal = $self->hal_from_customer($c, $customer, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -120,7 +130,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_customer($c, $customer, $form);
#my $hal = $self->hal_from_customer($c, $customer, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -152,6 +162,9 @@ sub PUT :Allow {
my $form = $self->get_form($c);
$customer = $self->update_customer($c, $customer, $old_resource, $resource, $form);
last unless $customer;
my $hal = $self->hal_from_customer($c, $customer, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -160,7 +173,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_customer($c, $customer, $form);
#my $hal = $self->hal_from_customer($c, $customer, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -200,6 +213,41 @@ sub DELETE :Allow {
}
=cut
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -216,6 +216,12 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to activate domain.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_domain = $self->item_by_id($c, $billing_domain->id);
return $self->hal_from_item($c,$_domain); });
$guard->commit;

@ -24,14 +24,19 @@ class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -107,6 +112,13 @@ sub DELETE :Allow {
$prov_domain->provisioning_voip_subscribers->delete;
$prov_domain->delete;
}
last unless $self->add_delete_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
#my $_domain = $self->item_by_id($c, $id);
return $self->hal_from_item($c,$domain); });
$domain->delete;
try {
@ -119,7 +131,7 @@ sub DELETE :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to deactivate domain.");
last;
}
$guard->commit;
$c->response->status(HTTP_NO_CONTENT);
@ -128,6 +140,41 @@ sub DELETE :Allow {
return;
}
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -185,6 +185,12 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create reseller.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_reseller = $self->reseller_by_id($c, $reseller->id);
return $self->hal_from_reseller($c, $_reseller, $form); });
$guard->commit;

@ -21,14 +21,19 @@ class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => 'admin',
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => 'admin',
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -107,6 +112,9 @@ sub PATCH :Allow {
$reseller = $self->update_reseller($c, $reseller, $old_resource, $resource, $form);
last unless $reseller;
my $hal = $self->hal_from_reseller($c, $reseller, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -114,7 +122,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_reseller($c, $reseller, $form);
#my $hal = $self->hal_from_reseller($c, $reseller, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -147,6 +155,9 @@ sub PUT :Allow {
$reseller = $self->update_reseller($c, $reseller, $old_resource, $resource, $form);
last unless $reseller;
my $hal = $self->hal_from_reseller($c, $reseller, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -154,7 +165,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_reseller($c, $reseller, $form);
#my $hal = $self->hal_from_reseller($c, $reseller, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -168,6 +179,41 @@ sub PUT :Allow {
# we don't allow to DELETE a reseller?
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -337,7 +337,14 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create subscriber.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_form = $self->get_form($c);
my $_subscriber = $self->item_by_id($c, $subscriber->id);
my $_resource = $self->resource_from_item($c, $_subscriber, $_form);
return $self->hal_from_item($c,$_subscriber,$_resource,$_form); });
$guard->commit;

@ -24,14 +24,19 @@ class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -111,6 +116,10 @@ sub PUT :Allow {
$subscriber = $self->update_item($c, $schema, $subscriber, $r, $resource, $form);
last unless $subscriber;
$resource = $self->resource_from_item($c, $subscriber, $form);
my $hal = $self->hal_from_item($c, $subscriber, $resource, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -118,8 +127,8 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
$resource = $self->resource_from_item($c, $subscriber, $form);
my $hal = $self->hal_from_item($c, $subscriber, $resource, $form);
#$resource = $self->resource_from_item($c, $subscriber, $form);
#my $hal = $self->hal_from_item($c, $subscriber, $resource, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -162,6 +171,10 @@ sub PATCH :Allow {
$subscriber = $self->update_item($c, $schema, $subscriber, $r, $resource, $form);
last unless $subscriber;
$resource = $self->resource_from_item($c, $subscriber, $form);
my $hal = $self->hal_from_item($c, $subscriber, $resource, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -169,8 +182,8 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
$resource = $self->resource_from_item($c, $subscriber, $form);
my $hal = $self->hal_from_item($c, $subscriber, $resource, $form);
#$resource = $self->resource_from_item($c, $subscriber, $form);
#my $hal = $self->hal_from_item($c, $subscriber, $resource, $form);
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
$hal->http_headers,
), $hal->as_json);
@ -199,8 +212,16 @@ sub DELETE :Allow {
}
}
last unless $self->add_delete_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_form = $self->get_form($c);
#my $_subscriber = $self->item_by_id($c, $id);
my $_resource = $self->resource_from_item($c, $subscriber, $_form);
return $self->hal_from_item($c,$subscriber,$_resource,$_form); });
NGCP::Panel::Utils::Subscriber::terminate(c => $c, subscriber => $subscriber);
$guard->commit;
$c->response->status(HTTP_NO_CONTENT);
@ -209,6 +230,41 @@ sub DELETE :Allow {
return;
}
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -137,7 +137,7 @@ sub OPTIONS :Allow {
sub POST :Allow {
my ($self, $c) = @_;
my $guard = $c->model('DB')->txn_scope_guard;
{
my $resource = $self->get_valid_post_data(
c => $c,
@ -165,6 +165,15 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create contact.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
#my $_form = $self->get_form($c);
my $_contact = $self->contact_by_id($c, $contact->id);
return $self->hal_from_contact($c, $_contact, $form); });
$guard->commit;
$c->response->status(HTTP_CREATED);
$c->response->header(Location => sprintf('/%s%d', $c->request->path, $contact->id));

@ -21,14 +21,19 @@ class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#
__PACKAGE__->config(
action => {
map { $_ => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => 'admin',
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }
} } @{ __PACKAGE__->allowed_methods }),
@{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => 'admin',
Does => [qw(ACL RequireSSL)],
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -107,6 +112,9 @@ sub PATCH :Allow {
$contact = $self->update_contact($c, $contact, $old_resource, $resource, $form);
last unless $contact;
my $hal = $self->hal_from_contact($c, $contact, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -114,7 +122,7 @@ sub PATCH :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_contact($c, $contact, $form);
#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);
@ -146,6 +154,9 @@ sub PUT :Allow {
my $form = $self->get_form($c);
$contact = $self->update_contact($c, $contact, $old_resource, $resource, $form);
last unless $contact;
my $hal = $self->hal_from_contact($c, $contact, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -154,7 +165,7 @@ sub PUT :Allow {
$c->response->header(Preference_Applied => 'return=minimal');
$c->response->body(q());
} else {
my $hal = $self->hal_from_contact($c, $contact, $form);
#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);
@ -180,6 +191,13 @@ sub DELETE :Allow {
$self->error($c, HTTP_LOCKED, "Contact is still in use.");
last;
} else {
last unless $self->add_delete_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;
my $_form = $self->get_form($c);
return $self->hal_from_contact($c, $contact, $_form); });
$contact->delete;
}
$guard->commit;
@ -190,6 +208,41 @@ sub DELETE :Allow {
return;
}
sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);
}
sub journalsitem_get :Journal {
my $self = shift @_;
return $self->handle_journalsitem_get(@_);
}
sub journals_options :Journal {
my $self = shift @_;
return $self->handle_journals_options(@_);
}
sub journalsitem_options :Journal {
my $self = shift @_;
return $self->handle_journalsitem_options(@_);
}
sub journals_head :Journal {
my $self = shift @_;
return $self->handle_journals_head(@_);
}
sub journalsitem_head :Journal {
my $self = shift @_;
return $self->handle_journalsitem_head(@_);
}
sub end : Private {
my ($self, $c) = @_;

@ -15,6 +15,11 @@ use Regexp::Common qw(delimited); # $RE{delimited}
use HTTP::Headers::Util qw(split_header_words);
use NGCP::Panel::Utils::ValidateJSON qw();
use NGCP::Panel::Utils::Journal qw();
#use boolean qw(true);
#use Data::HAL qw();
#use Data::HAL::Link qw();
has('last_modified', is => 'rw', isa => InstanceOf['DateTime']);
has('ctx', is => 'rw', isa => InstanceOf['NGCP::Panel']);
@ -271,14 +276,27 @@ sub allowed_methods_filtered {
sub allowed_methods {
my ($self) = @_;
#my $meta = $self->meta;
#my @allow;
#for my $method ($meta->get_method_list) {
# push @allow, $meta->get_method($method)->name
# if $meta->get_method($method)->can('attributes') &&
# grep { 'Allow' eq $_ } @{ $meta->get_method($method)->attributes };
#}
#return [sort @allow];
return $self->attributed_methods('Allow');
}
sub attributed_methods {
my ($self,$attribute) = @_;
my $meta = $self->meta;
my @allow;
my @attributed;
for my $method ($meta->get_method_list) {
push @allow, $meta->get_method($method)->name
push @attributed, $meta->get_method($method)->name
if $meta->get_method($method)->can('attributes') &&
grep { 'Allow' eq $_ } @{ $meta->get_method($method)->attributes };
grep { $attribute eq $_ } @{ $meta->get_method($method)->attributes };
}
return [sort @allow];
return [sort @attributed];
}
sub valid_id {
@ -474,15 +492,42 @@ around 'item_rs' => sub {
my ($orig, $self, @orig_params) = @_;
my $item_rs = $self->$orig(@orig_params);
return unless($item_rs);
if ($self->can('query_params')) {
return $self->apply_query_params($orig_params[0],$self->query_params,$item_rs);
}
return $item_rs;
## no query params defined in collection controller
#unless($self->can('query_params') && @{ $self->query_params }) {
# return $item_rs;
#}
#
#my $c = $orig_params[0];
#foreach my $param(keys %{ $c->req->query_params }) {
# my @p = grep { $_->{param} eq $param } @{ $self->query_params };
# next unless($p[0]->{query}); # skip "dummy" query parameters
# my $q = $c->req->query_params->{$param}; # TODO: arrayref?
# $q =~ s/\*/\%/g;
# if(@p) {
# #ctx config may be necessary
# $item_rs = $item_rs->search($p[0]->{query}->{first}($q,$self->ctx), $p[0]->{query}->{second}($q,$self->ctx));
# }
#}
#return $item_rs;
};
sub apply_query_params {
my ($self,$c,$query_params,$item_rs) = @_;
# no query params defined in collection controller
unless($self->can('query_params') && @{ $self->query_params }) {
unless(@{ $query_params }) {
return $item_rs;
}
my $c = $orig_params[0];
foreach my $param(keys %{ $c->req->query_params }) {
my @p = grep { $_->{param} eq $param } @{ $self->query_params };
my @p = grep { $_->{param} eq $param } @{ $query_params };
next unless($p[0]->{query}); # skip "dummy" query parameters
my $q = $c->req->query_params->{$param}; # TODO: arrayref?
$q =~ s/\*/\%/g;
@ -492,7 +537,8 @@ around 'item_rs' => sub {
}
}
return $item_rs;
};
};
sub is_true {
my ($self, $v) = @_;
@ -518,9 +564,69 @@ sub is_false {
return;
}
sub to_json {
my ($self, $data) = @_;
return JSON::to_json($data, { canonical => 1, pretty => 1, utf8 => 1 });
sub add_create_journal_item_hal {
my ($self,$c,@args) = @_;
return NGCP::Panel::Utils::Journal::add_journal_item_hal($self,$c,NGCP::Panel::Utils::Journal::CREATE_JOURNAL_OP,@args);
}
sub add_update_journal_item_hal {
my ($self,$c,@args) = @_;
return NGCP::Panel::Utils::Journal::add_journal_item_hal($self,$c,NGCP::Panel::Utils::Journal::UPDATE_JOURNAL_OP,@args);
}
sub add_delete_journal_item_hal {
my ($self,$c,@args) = @_;
return NGCP::Panel::Utils::Journal::add_journal_item_hal($self,$c,NGCP::Panel::Utils::Journal::DELETE_JOURNAL_OP,@args);
}
sub get_journal_action_config {
my ($class,$resource_name,$action_template) = @_;
my $cfg = NGCP::Panel::Utils::Journal::get_journal_resource_config(NGCP::Panel->config,$resource_name);
if ($cfg->{journal_resource_enabled}) {
return NGCP::Panel::Utils::Journal::get_api_journal_action_config('api/' . $resource_name,$action_template,$class->attributed_methods('Journal'));
}
return [];
}
sub get_journal_query_params {
my ($class,$query_params) = @_;
return NGCP::Panel::Utils::Journal::get_api_journal_query_params($query_params);
}
sub handle_item_base_journal {
return NGCP::Panel::Utils::Journal::handle_api_item_base_journal(@_);
}
sub handle_journals_get {
return NGCP::Panel::Utils::Journal::handle_api_journals_get(@_);
}
sub handle_journalsitem_get {
return NGCP::Panel::Utils::Journal::handle_api_journalsitem_get(@_);
}
sub handle_journals_options {
return NGCP::Panel::Utils::Journal::handle_api_journals_options(@_);
}
sub handle_journalsitem_options {
return NGCP::Panel::Utils::Journal::handle_api_journalsitem_options(@_);
}
sub handle_journals_head {
return NGCP::Panel::Utils::Journal::handle_api_journals_head(@_);
}
sub handle_journalsitem_head {
return NGCP::Panel::Utils::Journal::handle_api_journalsitem_head(@_);
}
sub get_journal_relation_link {
my $cfg = NGCP::Panel::Utils::Journal::get_journal_resource_config(NGCP::Panel->config,$_[0]->resource_name);
if ($cfg->{journal_resource_enabled}) {
return NGCP::Panel::Utils::Journal::get_journal_relation_link(@_);
}
return ();
}
1;

@ -66,6 +66,7 @@ sub hal_from_profile {
Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $profile->id)),
( map { Data::HAL::Link->new(relation => 'ngcp:billingfees', href => sprintf("/api/billingfees/%d", $_->id)) } $profile->billing_fees->all ),
( map { Data::HAL::Link->new(relation => 'ngcp:billingzones', href => sprintf("/api/billingzones/%d", $_->id)) } $profile->billing_zones->all ),
$self->get_journal_relation_link($profile->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -46,6 +46,7 @@ sub hal_from_item {
Data::HAL::Link->new(relation => 'self', href => sprintf("%s%s", $self->dispatch_path, $item->id)),
Data::HAL::Link->new(relation => "ngcp:$type", href => sprintf("/api/%s/%s", $type, $item->id)),
Data::HAL::Link->new(relation => 'ngcp:subscribers', href => sprintf("/api/subscribers/%d", $item->id)),
$self->get_journal_relation_link($item->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -84,6 +84,7 @@ sub hal_from_contract {
$billing_profile_id
? Data::HAL::Link->new(relation => 'ngcp:billingprofiles', href => sprintf("/api/billingprofiles/%d", $billing_profile_id))
: (),
$self->get_journal_relation_link($contract->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -56,6 +56,7 @@ sub hal_from_contact {
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)),
$self->get_journal_relation_link($contact->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -85,6 +85,7 @@ sub hal_from_customer {
$customer->invoice_email_template_id ? (Data::HAL::Link->new(relation => 'ngcp:invoiceemailtemplates', href => sprintf("/api/emailtemplates/%d", $customer->invoice_email_template_id))) : (),
$customer->invoice_template_id ? (Data::HAL::Link->new(relation => 'ngcp:invoicetemplates', href => sprintf("/api/invoicetemplates/%d", $customer->invoice_template_id))) : (),
Data::HAL::Link->new(relation => 'ngcp:calls', href => sprintf("/api/calls/?customer_id=%d", $customer->id)),
$self->get_journal_relation_link($customer->id),
],
relation => 'ngcp:'.$self->resource_name,
);
@ -116,15 +117,6 @@ sub customer_by_id {
sub update_customer {
my ($self, $c, $customer, $old_resource, $resource, $form) = @_;
my $old_hal = $self->hal_from_customer($c, $customer, $form);
$c->model('DB')->resultset('journals')->create({
type => "update",
resource => "customers",
resource_id => $customer->id,
timestamp => NGCP::Panel::Utils::DateTime::current_local->hires_epoch,
content => $self->to_json($old_hal->resource),
});
my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
$old_resource->{billing_profile_id} = $billing_mapping->billing_profile_id;
$old_resource->{prepaid} = $billing_mapping->billing_profile->prepaid;
@ -237,7 +229,7 @@ sub update_customer {
}
$customer->update($resource);
if(($customer->external_id // '') ne $old_ext_id) {
foreach my $sub($customer->voip_subscribers->all) {
my $prov_sub = $sub->provisioning_voip_subscriber;

@ -44,6 +44,7 @@ sub hal_from_item {
Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)),
#( map { $_->attribute->internal ? () : Data::HAL::Link->new(relation => 'ngcp:domainpreferences', href => sprintf("/api/domainpreferences/%d", $_->id), name => $_->attribute->attribute) } $item->provisioning_voip_domain->voip_dom_preferences->all ),
Data::HAL::Link->new(relation => 'ngcp:domainpreferences', href => sprintf("/api/domainpreferences/%d", $item->id)),
$self->get_journal_relation_link($item->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -53,6 +53,7 @@ sub hal_from_reseller {
Data::HAL::Link->new(relation => 'ngcp:soundsets', href => sprintf("/api/soundsets/?reseller_id=%d", $reseller->id)),
Data::HAL::Link->new(relation => 'ngcp:rewriterulesets', href => sprintf("/api/rewriterulesets/?reseller_id=%d", $reseller->id)),
Data::HAL::Link->new(relation => 'ngcp:pbxdevices', href => sprintf("/api/pbxdevices/?reseller_id=%d", $reseller->id)),
$self->get_journal_relation_link($reseller->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -135,6 +135,7 @@ sub hal_from_item {
Data::HAL::Link->new(relation => 'ngcp:reminders', href => sprintf("/api/reminders/?subscriber_id=%d", $item->id)),
Data::HAL::Link->new(relation => 'ngcp:callforwards', href => sprintf("/api/callforwards/%d", $item->id)),
#Data::HAL::Link->new(relation => 'ngcp:trustedsources', href => sprintf("/api/trustedsources/%d", $item->contract->id)),
$self->get_journal_relation_link($item->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -42,6 +42,8 @@ sub hal_from_contact {
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)),
$self->get_journal_relation_link($contact->id),
],
relation => 'ngcp:'.$self->resource_name,
);

@ -0,0 +1,621 @@
package NGCP::Panel::Utils::Journal;
use strict;
use warnings;
use Sipwise::Base;
use DBIx::Class::Exception;
use NGCP::Panel::Utils::DateTime;
use JSON;
use HTTP::Status qw(:constants);
use TryCatch;
use boolean qw(true);
use Data::HAL qw();
use Data::HAL::Link qw();
use Scalar::Util 'blessed';
use Storable;
use IO::Compress::Deflate qw($DeflateError);
use IO::Uncompress::Inflate qw();
use Sereal::Decoder qw();
use Sereal::Encoder qw();
use constant CREATE_JOURNAL_OP => 'create';
use constant UPDATE_JOURNAL_OP => 'update';
use constant DELETE_JOURNAL_OP => 'delete';
use constant _JOURNAL_OPS => { CREATE_JOURNAL_OP.'' => 1, UPDATE_JOURNAL_OP.'' => 1, DELETE_JOURNAL_OP.'' => 1 };
use constant OPERATION_DEFAULT_ENABLED => 0;
use constant API_JOURNAL_RESOURCE_NAME => 'journal';
use constant API_JOURNALITEMTOP_RESOURCE_NAME => 'recent'; #empty to disable
use constant JOURNAL_RESOURCE_DEFAULT_ENABLED => 0;
use constant CONTENT_JSON_FORMAT => 'json';
use constant CONTENT_STORABLE_FORMAT => 'storable';
use constant CONTENT_JSON_DEFLATE_FORMAT => 'json_deflate';
use constant CONTENT_SEREAL_FORMAT => 'sereal';
use constant _CONTENT_FORMATS => { CONTENT_JSON_FORMAT.'' => 1, CONTENT_STORABLE_FORMAT.'' => 1, CONTENT_JSON_DEFLATE_FORMAT.'' => 1, CONTENT_SEREAL_FORMAT.'' => 1 };
use constant CONTENT_DEFAULT_FORMAT => CONTENT_JSON_FORMAT;
use constant API_JOURNAL_RELATION => 'ngcp:'.API_JOURNAL_RESOURCE_NAME;
#use constant API_JOURNALITEM_RELATION => 'ngcp:journalitem';
sub add_journal_item_hal {
my ($controller,$c,$operation,@args) = @_;
my $cfg = _get_api_journal_op_config($c,$controller->resource_name,$operation);
if ($cfg->{operation_enabled}) {
my $arg = $args[0];
my @hal_from_item = ();
my $params;
if (ref $arg eq 'HASH') {
$params = $arg;
my $h = (defined $params->{hal} ? $params->{hal} : $params->{hal_from_item});
$h //= (defined $params->{resource} ? $params->{resource} : $params->{resource_from_item});
if (defined $h) {
if (ref $h eq 'ARRAY') {
@hal_from_item = @$h;
} else { #if (not ref $h) {
@hal_from_item = ( $h );
}
}
} elsif (ref $arg eq 'ARRAY') {
$params = {};
@hal_from_item = @$arg;
} else {
$params = {};
@hal_from_item = @args;
}
my $code = shift @hal_from_item;
unshift(@hal_from_item,$c);
unshift(@hal_from_item,$controller);
unshift(@hal_from_item,$code);
$params->{hal_from_item} = \@hal_from_item;
$params->{operation} = $operation;
$params->{resource_name} //= $controller->resource_name;
$params->{format} //= $cfg->{format};
my $resource = shift @hal_from_item;
my $hal;
if (ref $resource eq 'CODE') {
$hal = $resource->(@hal_from_item);
} else {
$hal = $resource;
}
if (ref $hal eq Data::HAL::) {
$resource = $hal->resource;
}
my $id;
if (ref $resource eq 'HASH') {
$id = $resource->{id};
} elsif ((defined blessed($resource)) && $resource->can('id')) {
$id = $resource->id;
}
return _create_journal($controller,$c,$id,$resource,$params);
}
return 1;
}
sub get_api_journal_action_config {
my ($path_part,$action_template,$journal_methods) = @_;
my %journal_actions_found = map { $_ => 1 } @$journal_methods;
if (exists $journal_actions_found{'item_base_journal'}) {
my @result = ();
if (exists $journal_actions_found{'journals_get'}) {
my $action_config = Storable::dclone($action_template);
$action_config->{Chained} = 'item_base_journal';
$action_config->{PathPart} //= API_JOURNAL_RESOURCE_NAME;
$action_config->{Args} = 0;
$action_config->{Method} = 'GET';
push(@result,$action_config,'journals_get');
}
if (exists $journal_actions_found{'journals_options'}) {
my $action_config = Storable::dclone($action_template);
$action_config->{Chained} = 'item_base_journal';
$action_config->{PathPart} //= API_JOURNAL_RESOURCE_NAME;
$action_config->{Args} = 0;
$action_config->{Method} = 'OPTIONS';
push(@result,$action_config,'journals_options');
}
if (exists $journal_actions_found{'journals_head'}) {
my $action_config = Storable::dclone($action_template);
$action_config->{Chained} = 'item_base_journal';
$action_config->{PathPart} //= API_JOURNAL_RESOURCE_NAME;
$action_config->{Args} = 0;
$action_config->{Method} = 'HEAD';
push(@result,$action_config,'journals_head');
}
if (exists $journal_actions_found{'journalsitem_get'}) {
my $action_config = Storable::dclone($action_template);
$action_config->{Chained} = 'item_base_journal';
$action_config->{PathPart} //= API_JOURNAL_RESOURCE_NAME;
$action_config->{Args} = 1;
$action_config->{Method} = 'GET';
push(@result,$action_config,'journalsitem_get');
}
if (exists $journal_actions_found{'journalsitem_options'}) {
my $action_config = Storable::dclone($action_template);
$action_config->{Chained} = 'item_base_journal';
$action_config->{PathPart} //= API_JOURNAL_RESOURCE_NAME;
$action_config->{Args} = 1;
$action_config->{Method} = 'OPTIONS';
push(@result,$action_config,'journalsitem_options');
}
if (exists $journal_actions_found{'journalsitem_head'}) {
my $action_config = Storable::dclone($action_template);
$action_config->{Chained} = 'item_base_journal';
$action_config->{PathPart} //= API_JOURNAL_RESOURCE_NAME;
$action_config->{Args} = 1;
$action_config->{Method} = 'HEAD';
push(@result,$action_config,'journalsitem_head');
}
if ((scalar @result) > 0) {
push(@result,{
Chained => '/',
PathPart => $path_part,
CaptureArgs => 1,
},'item_base_journal');
@result = reverse @result;
return \@result;
}
}
return [];
}
sub get_api_journal_query_params {
my ($query_params) = @_;
my @params = (defined $query_params ? @$query_params : ());
push(@params,{
param => 'operation',
description => 'Filter for journal items by a specific CRUD operation ("create", "update" or "delete")',
query => {
first => sub {
my $q = shift;
{ 'operation' => $q };
},
second => sub { },
},
});
return ['journal_query_params',
is => 'ro',
isa => 'ArrayRef',
default => sub { #[ #sub {[
\@params
}, #], #]},
];
}
sub handle_api_item_base_journal {
my ($controller,$c,$id) = @_;
$c->stash->{item_id_journal} = $id;
return undef;
}
sub handle_api_journals_get {
my ($controller,$c) = @_;
my $page = $c->request->params->{page} // 1;
my $rows = $c->request->params->{rows} // 10;
{
my $item_id = $c->stash->{item_id_journal};
last unless $controller->valid_id($c, $item_id);
my $journals = get_journal_rs($controller,$c,$item_id);
(my $total_count, $journals) = $controller->paginate_order_collection($c,$journals);
my (@embedded, @links);
for my $journal($journals->all) {
my $hal = hal_from_journal($controller,$c,$journal);
$hal->_forcearray(1);
push @embedded,$hal;
my $link = get_journal_relation_link($journal,$item_id,$journal->id);
$link->_forcearray(1);
push @links,$link;
}
push @links,
Data::HAL::Link->new(
relation => 'curies',
href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}',
name => 'ngcp',
templated => true,
),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/');
push @links, $controller->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params);
my $hal = Data::HAL->new(
embedded => [@embedded],
links => [@links],
);
$hal->resource({
total_count => $total_count,
});
my $response = HTTP::Response->new(HTTP_OK, undef,
HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json);
$c->response->headers($response->headers);
$c->response->body($response->content);
return;
}
return;
}
sub handle_api_journalsitem_get {
my ($controller,$c,$id) = @_;
{
my $item_id = $c->stash->{item_id_journal};
last unless $controller->valid_id($c, $item_id);
my $journal = undef;
if (API_JOURNALITEMTOP_RESOURCE_NAME and $id eq API_JOURNALITEMTOP_RESOURCE_NAME) {
$journal = get_top_journalitem($controller,$c,$item_id);
} elsif ($controller->valid_id($c, $id)) {
$journal = get_journalitem($c,$id);
} else {
last;
}
last unless $controller->resource_exists($c, journal => $journal);
if ($journal->resource_id != $item_id) {
$c->log->error("Journal item '" . $id . "' does not belong to '" . $controller->resource_name . '/' . $item_id . "'");
$controller->error($c, HTTP_NOT_FOUND, "Entity 'journal' not found.");
return;
}
my $hal = hal_from_journal($controller,$c,$journal);
#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(HTTP::Headers->new($hal->http_headers));
$c->response->body($hal->as_json);
return;
}
return;
}
sub handle_api_journals_options {
my ($controller, $c, $id) = @_;
my @allowed_methods = ('OPTIONS');
my %journal_actions_found = map { $_ => 1 } @{ $controller->attributed_methods('Journal') };
if (exists $journal_actions_found{'journals_get'}) {
push(@allowed_methods,'GET');
if (exists $journal_actions_found{'journals_head'}) {
push(@allowed_methods,'HEAD');
}
}
$c->response->headers(HTTP::Headers->new(
Allow => join(', ',@allowed_methods),
));
$c->response->content_type('application/json');
$c->response->body(JSON::to_json({ methods => \@allowed_methods })."\n");
return;
}
sub handle_api_journalsitem_options {
my ($controller, $c, $id) = @_;
my @allowed_methods = ('OPTIONS');
my %journal_actions_found = map { $_ => 1 } @{ $controller->attributed_methods('Journal') };
if (exists $journal_actions_found{'journalsitem_get'}) {
push(@allowed_methods,'GET');
if (exists $journal_actions_found{'journalsitem_head'}) {
push(@allowed_methods,'HEAD');
}
}
$c->response->headers(HTTP::Headers->new(
Allow => join(', ',@allowed_methods),
));
$c->response->content_type('application/json');
$c->response->body(JSON::to_json({ methods => \@allowed_methods })."\n");
return;
}
sub _has_journal_method {
my ($controller, $action) = @_;
my %journal_actions_found = map { $_ => 1 } @{ $controller->attributed_methods('Journal') };
return exists $journal_actions_found{$action};
}
sub handle_api_journals_head {
my ($controller, $c) = @_;
if (_has_journal_method($controller,'journals_get')) {
$c->forward('journals_get');
$c->response->body(q());
return;
} else {
$c->log->error("journals_get action not implemented: " . ref $controller);
$c->response->status(HTTP_METHOD_NOT_ALLOWED);
$c->response->body(q());
return;
}
}
sub handle_api_journalsitem_head {
my ($controller, $c) = @_;
if (_has_journal_method($controller,'journalsitem_get')) {
$c->forward('journalsitem_get');
$c->response->body(q());
return;
} else {
$c->log->error("journalsitem_get not implemented: " . ref $controller);
$c->response->status(HTTP_METHOD_NOT_ALLOWED);
$c->response->body(q());
return;
}
}
sub get_journal_rs {
my ($controller,$c,$resource_id,$all_columns) = @_;
my $rs = $c->model('DB')->resultset('journals')
->search({
'resource_name' => $controller->resource_name,
'resource_id' => $resource_id,
},($all_columns ? undef : {
columns => [qw(id operation resource_name resource_id timestamp username)],
#alias => 'me',
}));
if ($controller->can('journal_query_params')) {
return $controller->apply_query_params($c,$controller->journal_query_params,$rs);
}
return $rs;
}
sub get_journalitem {
my ($c,$id) = @_;
my $journal = $c->model('DB')->resultset('journals')
->find({
'id' => $id,
});
return $journal;
}
sub get_top_journalitem {
my ($controller,$c,$resource_id) = @_;
return get_top_n_journalitems($controller,$c,$resource_id,1,1)->[0];
}
sub get_top_n_journalitems {
my ($controller,$c,$resource_id,$n,$all_columns) = @_;
my @journals = get_journal_rs($controller,$c,$resource_id,$all_columns)->search(undef,{
page => 1,
rows => $n,
order_by => {-desc => "id"},
})->all;
return \@journals;
}
sub hal_from_journal {
my ($controller,$c,$journal) = @_;
my %resource = $journal->get_inflated_columns;
{
if (exists $resource{content}) {
try {
$resource{content} = _deserialize_content($journal->content_format,$journal->content);
} catch($e) {
$resource{content} = undef;
$c->log->error("Failed to de-serialize content snapshot of journal item '" . $journal->id . "': $e");
#$controller->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
#return;
last;
};
#delta stuff...
}
}
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,
),
get_journal_relation_link($journal,$journal->resource_id,undef,'collection'),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
get_journal_relation_link($journal,$journal->resource_id,$journal->id,'self'),
Data::HAL::Link->new(relation => sprintf('ngcp:%s',$journal->resource_name),
href => sprintf('/api/%s/%d', $journal->resource_name, $journal->resource_id)),
],
relation => API_JOURNAL_RELATION, #API_JOURNALITEM_RELATION,
);
my $datetime_fmt = DateTime::Format::Strptime->new(
pattern => '%F %T',
);
$resource{timestamp} = $datetime_fmt->format_datetime($resource{timestamp});
delete $resource{resource_name};
delete $resource{resource_id};
delete $resource{content_format};
$hal->resource({%resource});
return $hal;
}
sub get_journal_relation_link {
my ($resource,$item_id,$id,$relation) = @_;
my $resource_name = undef;
if (ref $resource eq 'HASH') {
$resource_name = $resource->{resource_name};
} elsif ((defined blessed($resource)) && $resource->can('resource_name')) { #both controllers and journal rows
$resource_name = $resource->resource_name;
} elsif (!ref $resource) {
$resource_name = $resource;
}
if (defined $resource_name) {
if (defined $id) {
return Data::HAL::Link->new(
relation => ($relation // API_JOURNAL_RELATION), #API_JOURNALITEM_RELATION),
href => sprintf('/api/%s/%d/%s/%d', $resource_name, $item_id,API_JOURNAL_RESOURCE_NAME,$id),
);
} else {
return Data::HAL::Link->new(
relation => ($relation // API_JOURNAL_RELATION),
href => sprintf('/api/%s/%d/%s/', $resource_name, $item_id,API_JOURNAL_RESOURCE_NAME),
)
}
}
return undef;
}
sub get_api_journal_op_config {
my ($config,$resource_name,$operation) = @_;
return _get_journal_op_config($config->{api_journal},undef,$resource_name,$operation);
}
sub _get_api_journal_op_config {
return _get_journal_op_config($_[0]->config->{api_journal},@_);
}
sub _get_journal_op_config {
my ($cfg,$c,$resource_name,$operation) = @_;
#my $cfg = $c->config->{$section};
my $format = CONTENT_DEFAULT_FORMAT;
my $operation_enabled = OPERATION_DEFAULT_ENABLED;
if (defined $cfg && exists $cfg->{$resource_name}) {
$cfg = $cfg->{$resource_name};
if (ref $cfg eq 'HASH') {
if (ref $cfg->{operations} eq 'ARRAY') {
foreach my $op (@{ $cfg->{operations} }) {
if (exists _JOURNAL_OPS->{$op}) {
if ($operation eq $op) {
$operation_enabled = 1;
last;
}
} else {
$c->log->error("Invalid '$resource_name' operation to log in journals: $op") if defined $c;
}
}
}
if (defined $cfg->{format}) {
if (exists _CONTENT_FORMATS->{$cfg->{format}}) {
$format = $cfg->{format};
} else {
$c->log->error("Invalid journal content format for resource '$resource_name': $cfg->{format}") if defined $c;
}
}
}
}
return { format => $format, operation_enabled => $operation_enabled};
}
sub get_journal_resource_config {
my ($config,$resource_name) = @_;
my $cfg = $config->{api_journal};
my $journal_resource_enabled = JOURNAL_RESOURCE_DEFAULT_ENABLED;
if (defined $cfg && exists $cfg->{$resource_name}) {
$cfg = $cfg->{$resource_name};
if (ref $cfg eq 'HASH') {
if (defined $cfg->{enabled}) {
if ($cfg->{enabled}) {
$journal_resource_enabled = 1;
} else {
$journal_resource_enabled = 0;
}
}
}
}
return { journal_resource_enabled => $journal_resource_enabled };
}
sub _serialize_content { #run this in eval only, deflate somehow inflicts a segfault in subsequent catalyst action when not consuming all args there.
my ($format,$data) = @_;
if (defined $format && defined $data) {
if ($format eq CONTENT_JSON_FORMAT) {
return JSON::to_json($data, { canonical => 1, pretty => 1, utf8 => 1 });
} elsif ($format eq CONTENT_JSON_DEFLATE_FORMAT) {
my $json = JSON::to_json($data, { canonical => 1, pretty => 1, utf8 => 1 });
my $buf = '';
IO::Compress::Deflate::deflate(\$json,\$buf) or die($DeflateError);
return $buf;
} elsif ($format eq CONTENT_STORABLE_FORMAT) {
return Storable::nfreeze($data);
} elsif ($format eq CONTENT_SEREAL_FORMAT) {
return Sereal::Encoder::encode_sereal($data);
}
}
return undef;
}
sub _deserialize_content {
my ($format,$serialized) = @_;
if (defined $format && defined $serialized) {
if ($format eq CONTENT_JSON_FORMAT) {
return JSON::from_json($serialized);
} elsif ($format eq CONTENT_JSON_DEFLATE_FORMAT) {
my $buf = '';
IO::Uncompress::Inflate::inflate(\$serialized,\$buf) or die($DeflateError);
return JSON::from_json($buf);
} elsif ($format eq CONTENT_STORABLE_FORMAT) {
return Storable::thaw($serialized);
} elsif ($format eq CONTENT_SEREAL_FORMAT) {
return Sereal::Decoder::decode_sereal($serialized);
}
}
return undef;
}
sub _create_journal {
my ($controller,$c,$id,$resource,$params) = @_;
{
my $content_format = ((defined $params->{format} and exists _CONTENT_FORMATS->{$params->{format}}) ? $params->{format} : CONTENT_DEFAULT_FORMAT);
my $operation = ((defined $params->{operation} and exists _JOURNAL_OPS->{$params->{operation}}) ? $params->{operation} : undef);
if (!defined $operation) {
$c->log->error("Invalid '$params->{resource_name}' operation to log in journals: $operation");
last;
}
my %journal = (
operation => $operation,
resource_name => $params->{resource_name},
#resource_id => $id,
timestamp => NGCP::Panel::Utils::DateTime::current_local->hires_epoch,
content_format => $content_format,
);
if (defined $id) {
$journal{resource_id} = $id;
}
if (defined $params->{user}) {
$journal{username} = $params->{user};
} elsif (defined $c->user) {
if($c->user->roles eq 'admin' || $c->user->roles eq 'reseller') {
$journal{username} = $c->user->login;
} elsif($c->user->roles eq 'subscriber' || $c->user->roles eq 'subscriberadmin') {
$journal{username} = $c->user->webusername . '@' . $c->user->domain->domain;
}
}
try {
$journal{content} = _serialize_content($content_format,$resource);
#my $test = 1 / 0;
} catch($e) {
$c->log->error("Failed to serialize journal content snapshot of '" . $params->{resource_name} . '/' . $id . "': $e");
#$controller->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
#return;
last;
};
try {
return $c->model('DB')->resultset('journals')->create(\%journal);
} catch($e) {
$c->log->error("Failed to create journal item for '" . $params->{resource_name} . '/' . $id . "': $e");
#$controller->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
#return;
last;
};
}
return undef;
}
1;

@ -145,3 +145,63 @@ log4perl.appender.Default.layout.ConversionPattern=%d{ISO8601} [%p] [%F +%L] %m{
port 9200
</elasticsearch>
<api_journal>
<billingprofiles>
operations create
operations update
operations delete
format sereal
enabled 1
</billingprofiles>
<systemcontacts>
operations create
operations update
operations delete
format sereal
enabled 1
</systemcontacts>
<contracts>
operations create
operations update
operations delete
format sereal
enabled 1
</contracts>
<resellers>
operations create
operations update
operations delete
format sereal
enabled 1
</resellers>
<customercontacts>
operations create
operations update
operations delete
format sereal
enabled 1
</customercontacts>
<customers>
operations create
operations update
operations delete
format sereal
enabled 1
</customers>
<domains>
operations create
operations update
operations delete
format sereal
enabled 1
</domains>
<subscribers>
operations create
operations update
operations delete
format sereal
enabled 1
</subscribers>
</api_journal>

@ -1 +1 @@
PERL5LIB=/opt/Komodo-IDE-8/remote_debugging PERLDB_OPTS="RemotePort=127.0.0.1:42510" DBGP_IDEKEY="jdoe" CATALYST_DEBUG=1 DBIC_TRACE=1 DBIC_TRACE_PROFILE=console DEVEL_CONFESS_OPTIONS='objects builtin dump color source' perl -d `which plackup` -I ../data-hal/lib -I ../ngcp-schema/lib -I lib -I ../sipwise-base/lib/ ngcp_panel.psgi --listen /tmp/ngcp_panel_sock --nproc 1 -s FCGI -r
PERL5LIB=/opt/Komodo-IDE-8/remote_debugging PERLDB_OPTS="RemotePort=127.0.0.1:9000" DBGP_IDEKEY="jdoe" CATALYST_DEBUG=1 DBIC_TRACE=1 DBIC_TRACE_PROFILE=console DEVEL_CONFESS_OPTIONS='objects builtin dump color source' perl -d `which plackup` -I ../data-hal/lib -I ../ngcp-schema/lib -I lib -I ../sipwise-base/lib/ ngcp_panel.psgi --listen /tmp/ngcp_panel_sock --nproc 1 -s FCGI -r

@ -0,0 +1,843 @@
use Sipwise::Base;
use Net::Domain qw(hostfqdn);
use LWP::UserAgent;
use JSON qw();
use Test::More;
use Storable qw();
use LWP::Debug;
BEGIN {
unshift(@INC,'../lib');
}
use NGCP::Panel::Utils::Journal qw();
use Config::General;
my $catalyst_config = Config::General->new("../ngcp_panel.conf"); #take paths from configloader..
my %config = $catalyst_config->getall();
my $enable_journal_tests = 1;
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;
#$ua->ssl_opts(
# SSL_cert_file => $valid_ssl_client_cert,
# SSL_key_file => $valid_ssl_client_key,
# SSL_ca_file => $ssl_ca_cert,
#);
$ua->ssl_opts(
verify_hostname => 0,
);
$ua->credentials("127.0.0.1:4443", "api_admin_http", 'administrator', 'administrator');
#$ua->timeout(500); #useless, need to change the nginx timeout
my $t = time;
my $default_reseller_id = 1;
my $billingprofile = test_billingprofile($t,$default_reseller_id);
my $systemcontact = test_systemcontact($t);
my $contract = test_contract($billingprofile,$systemcontact);
(my $reseller,$billingprofile) = test_reseller($t,$contract);
my $domain = test_domain($t,$reseller);
my $customercontact = test_customercontact($t,$reseller);
my $customer = test_customer($customercontact,$billingprofile);
my $subscriber = test_subscriber($t,$customer,$domain);
done_testing;
sub test_billingprofile {
my ($t,$reseller_id) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/');
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
name => "test profile $t",
handle => "testprofile$t",
reseller_id => $reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "POST test billing profile");
my $billingprofile_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $billingprofile_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed billing profile");
my $billingprofile = JSON::from_json($res->decoded_content);
_test_item_journal_link('billingprofiles',$billingprofile);
_test_journal_options_head('billingprofiles',$billingprofile->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('billingprofiles',$billingprofile->{id},$billingprofile,'create',$journals);
_test_journal_options_head('billingprofiles',$billingprofile->{id},$journal->{id});
$req = HTTP::Request->new('PUT', $billingprofile_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
name => "test profile $t PUT",
handle => "testprofile$t",
reseller_id => $reseller_id,
}));
$res = $ua->request($req);
is($res->code, 200, "PUT test billingprofile");
$req = HTTP::Request->new('GET', $billingprofile_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PUT test billingprofile");
$billingprofile = JSON::from_json($res->decoded_content);
_test_item_journal_link('billingprofiles',$billingprofile);
$journal = _test_journal_top_journalitem('billingprofiles',$billingprofile->{id},$billingprofile,'update',$journals,$journal);
$req = HTTP::Request->new('PATCH', $billingprofile_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/name', value => "test profile $t PATCH" } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test billingprofile");
$req = HTTP::Request->new('GET', $billingprofile_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test billingprofile");
$billingprofile = JSON::from_json($res->decoded_content);
_test_item_journal_link('billingprofiles',$billingprofile);
$journal = _test_journal_top_journalitem('billingprofiles',$billingprofile->{id},$billingprofile,'update',$journals,$journal);
_test_journal_collection('billingprofiles',$billingprofile->{id},$journals);
return $billingprofile;
}
sub test_contract {
my ($billingprofile,$systemcontact) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/contracts/');
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
status => "active",
contact_id => $systemcontact->{id},
type => "reseller",
billing_profile_id => $billingprofile->{id},
}));
$res = $ua->request($req);
is($res->code, 201, "POST test reseller contract");
my $contract_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $contract_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test reseller contract");
my $contract = JSON::from_json($res->decoded_content);
_test_item_journal_link('contracts',$contract);
_test_journal_options_head('contracts',$contract->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('contracts',$contract->{id},$contract,'create',$journals);
_test_journal_options_head('contracts',$contract->{id},$journal->{id});
$req = HTTP::Request->new('PUT', $contract_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
status => "active",
contact_id => $systemcontact->{id},
type => "reseller",
billing_profile_id => $billingprofile->{id},
external_id => int(rand(10)),
}));
$res = $ua->request($req);
is($res->code, 200, "PUT test reseller contract");
$req = HTTP::Request->new('GET', $contract_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PUT test reseller contract");
$contract = JSON::from_json($res->decoded_content);
_test_item_journal_link('contracts',$contract);
$journal = _test_journal_top_journalitem('contracts',$contract->{id},$contract,'update',$journals,$journal);
$req = HTTP::Request->new('PATCH', $contract_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/external_id', value => int(rand(10)) } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test reseller contract");
$req = HTTP::Request->new('GET', $contract_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test reseller contract");
$contract = JSON::from_json($res->decoded_content);
_test_item_journal_link('contracts',$contract);
$journal = _test_journal_top_journalitem('contracts',$contract->{id},$contract,'update',$journals,$journal);
_test_journal_collection('contracts',$contract->{id},$journals);
return $contract;
}
sub test_customercontact {
my ($t,$reseller) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_".($t-1)."_first",
lastname => "cust_contact_".($t-1)."_last",
email => "cust_contact_".($t-1)."\@custcontact.invalid",
reseller_id => $reseller->{id},
}));
$res = $ua->request($req);
is($res->code, 201, "POST test customercontact");
my $customercontact_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $customercontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test customercontact");
my $customercontact = JSON::from_json($res->decoded_content);
_test_item_journal_link('customercontacts',$customercontact);
_test_journal_options_head('customercontacts',$customercontact->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('customercontacts',$customercontact->{id},$customercontact,'create',$journals);
_test_journal_options_head('customercontacts',$customercontact->{id},$journal->{id});
$req = HTTP::Request->new('PUT', $customercontact_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
firstname => "cust_contact_".($t-1)."_first_put",
lastname => "cust_contact_".($t-1)."_last_put",
email => "cust_contact_".($t-1)."_put\@custcontact.invalid",
reseller_id => $reseller->{id},
external_id => int(rand(10)),
}));
$res = $ua->request($req);
is($res->code, 200, "PUT test customercontact");
$req = HTTP::Request->new('GET', $customercontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PUT test customercontact");
$customercontact = JSON::from_json($res->decoded_content);
_test_item_journal_link('customercontacts',$customercontact);
$journal = _test_journal_top_journalitem('customercontacts',$customercontact->{id},$customercontact,'update',$journals,$journal);
$req = HTTP::Request->new('PATCH', $customercontact_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/firstname', value => "cust_contact_".($t-1)."_first_patch" } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test customercontact");
$req = HTTP::Request->new('GET', $customercontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test customercontact");
$customercontact = JSON::from_json($res->decoded_content);
_test_item_journal_link('customercontacts',$customercontact);
$journal = _test_journal_top_journalitem('customercontacts',$customercontact->{id},$customercontact,'update',$journals,$journal);
$req = HTTP::Request->new('DELETE', $customercontact_uri);
$res = $ua->request($req);
is($res->code, 204, "delete POSTed test customercontact");
#$domain = JSON::from_json($res->decoded_content);
$journal = _test_journal_top_journalitem('customercontacts',$customercontact->{id},$customercontact,'delete',$journals,$journal);
_test_journal_collection('customercontacts',$customercontact->{id},$journals);
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_".$t."_first",
lastname => "cust_contact_".$t."_last",
email => "cust_contact_".$t."\@custcontact.invalid",
reseller_id => $reseller->{id},
}));
$res = $ua->request($req);
is($res->code, 201, "POST another test customercontact");
$customercontact_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $customercontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test customercontact");
$customercontact = JSON::from_json($res->decoded_content);
return $customercontact;
}
sub test_reseller {
my ($t,$contract) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/resellers/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
contract_id => $contract->{id},
name => "test reseller $t",
status => "active",
}));
$res = $ua->request($req);
is($res->code, 201, "POST test reseller");
my $reseller_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $reseller_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test reseller");
my $reseller = JSON::from_json($res->decoded_content);
_test_item_journal_link('resellers',$reseller);
_test_journal_options_head('resellers',$reseller->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('resellers',$reseller->{id},$reseller,'create',$journals);
_test_journal_options_head('resellers',$reseller->{id},$journal->{id});
$req = HTTP::Request->new('PUT', $reseller_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
contract_id => $contract->{id},
name => "test reseller $t PUT",
status => "active",
}));
$res = $ua->request($req);
is($res->code, 200, "PUT test reseller");
$req = HTTP::Request->new('GET', $reseller_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PUT test reseller");
$reseller = JSON::from_json($res->decoded_content);
_test_item_journal_link('resellers',$reseller);
$journal = _test_journal_top_journalitem('resellers',$reseller->{id},$reseller,'update',$journals,$journal);
$req = HTTP::Request->new('PATCH', $reseller_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/name', value => "test reseller $t PATCH" } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test reseller");
$req = HTTP::Request->new('GET', $reseller_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test reseller");
$reseller = JSON::from_json($res->decoded_content);
_test_item_journal_link('resellers',$reseller);
$journal = _test_journal_top_journalitem('resellers',$reseller->{id},$reseller,'update',$journals,$journal);
#$req = HTTP::Request->new('DELETE', $reseller_uri);
#$res = $ua->request($req);
#is($res->code, 204, "delete POSTed test reseller");
##$domain = JSON::from_json($res->decoded_content);
#
#$journal = _test_journal_top_journalitem('resellers',$reseller->{id},$reseller,'delete',$journals,$journal);
_test_journal_collection('resellers',$reseller->{id},$journals);
#$req = HTTP::Request->new('POST', $uri.'/api/resellers/');
#$req->header('Content-Type' => 'application/json');
#$req->content(JSON::to_json({
# contract_id => $contract->{id},
# name => "test reseller $t 1",
# status => "active",
#}));
#$res = $ua->request($req);
#is($res->code, 201, "POST another test reseller");
#$reseller_uri = $uri.'/'.$res->header('Location');
#$req = HTTP::Request->new('GET', $reseller_uri);
#$res = $ua->request($req);
#is($res->code, 200, "fetch POSTed test reseller");
#$reseller = JSON::from_json($res->decoded_content);
my $billingprofile_uri = $uri.'/api/billingprofiles/'.$contract->{billing_profile_id};
$req = HTTP::Request->new('PATCH', $billingprofile_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/reseller_id', value => $reseller->{id} } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test billingprofile");
$req = HTTP::Request->new('GET', $billingprofile_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test billingprofile");
my $billingprofile = JSON::from_json($res->decoded_content);
return ($reseller,$billingprofile);
}
sub test_systemcontact {
my ($t) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/systemcontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "syst_contact_".($t-1)."_first",
lastname => "syst_contact_".($t-1)."_last",
email => "syst_contact_".($t-1)."\@systcontact.invalid",
}));
$res = $ua->request($req);
is($res->code, 201, "POST test systemcontact");
my $systemcontact_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $systemcontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test systemcontact");
my $systemcontact = JSON::from_json($res->decoded_content);
_test_item_journal_link('systemcontacts',$systemcontact);
_test_journal_options_head('systemcontacts',$systemcontact->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('systemcontacts',$systemcontact->{id},$systemcontact,'create',$journals);
_test_journal_options_head('systemcontacts',$systemcontact->{id},$journal->{id});
$req = HTTP::Request->new('PUT', $systemcontact_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
firstname => "syst_contact_".($t-1)."_first_put",
lastname => "syst_contact_".($t-1)."_last_put",
email => "syst_contact_".($t-1)."_put\@systcontact.invalid",
external_id => int(rand(10)),
}));
$res = $ua->request($req);
is($res->code, 200, "PUT test systemcontact");
$req = HTTP::Request->new('GET', $systemcontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PUT test systemcontact");
$systemcontact = JSON::from_json($res->decoded_content);
_test_item_journal_link('systemcontacts',$systemcontact);
$journal = _test_journal_top_journalitem('systemcontacts',$systemcontact->{id},$systemcontact,'update',$journals,$journal);
$req = HTTP::Request->new('PATCH', $systemcontact_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/firstname', value => "syst_contact_".($t-1)."_first_patch" } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test systemcontact");
$req = HTTP::Request->new('GET', $systemcontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test systemcontact");
$systemcontact = JSON::from_json($res->decoded_content);
_test_item_journal_link('systemcontacts',$systemcontact);
$journal = _test_journal_top_journalitem('systemcontacts',$systemcontact->{id},$systemcontact,'update',$journals,$journal);
$req = HTTP::Request->new('DELETE', $systemcontact_uri);
$res = $ua->request($req);
is($res->code, 204, "delete POSTed test systemcontact");
#$domain = JSON::from_json($res->decoded_content);
$journal = _test_journal_top_journalitem('systemcontacts',$systemcontact->{id},$systemcontact,'delete',$journals,$journal);
_test_journal_collection('systemcontacts',$systemcontact->{id},$journals);
$req = HTTP::Request->new('POST', $uri.'/api/systemcontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "syst_contact_".$t."_first",
lastname => "syst_contact_".$t."_last",
email => "syst_contact_".$t."\@systcontact.invalid",
}));
$res = $ua->request($req);
is($res->code, 201, "POST another test systemcontact");
$systemcontact_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $systemcontact_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test systemcontact");
$systemcontact = JSON::from_json($res->decoded_content);
return $systemcontact;
}
sub test_domain {
my ($t,$reseller) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/domains/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
domain => 'test' . ($t-1) . '.example.org',
reseller_id => $reseller->{id},
}));
$res = $ua->request($req);
is($res->code, 201, "POST test domain");
my $domain_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $domain_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test domain");
my $domain = JSON::from_json($res->decoded_content);
_test_item_journal_link('domains',$domain);
_test_journal_options_head('domains',$domain->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('domains',$domain->{id},$domain,'create',$journals);
_test_journal_options_head('domains',$domain->{id},$journal->{id});
$req = HTTP::Request->new('DELETE', $domain_uri);
$res = $ua->request($req);
is($res->code, 204, "delete POSTed test domain");
#$domain = JSON::from_json($res->decoded_content);
$journal = _test_journal_top_journalitem('domains',$domain->{id},$domain,'delete',$journals,$journal);
_test_journal_collection('domains',$domain->{id},$journals);
$req = HTTP::Request->new('POST', $uri.'/api/domains/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
domain => 'test' . $t . '.example.org',
reseller_id => $reseller->{id},
}));
$res = $ua->request($req);
is($res->code, 201, "POST another test domain");
$domain_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $domain_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test domain");
$domain = JSON::from_json($res->decoded_content);
return $domain;
}
sub test_customer {
my ($customer_contact,$billing_profile) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/customers/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
status => "active",
contact_id => $customer_contact->{id},
type => "sipaccount",
billing_profile_id => $billing_profile->{id},
max_subscribers => undef,
external_id => undef,
}));
$res = $ua->request($req);
is($res->code, 201, "POST test customer");
my $customer_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $customer_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test customer");
my $customer = JSON::from_json($res->decoded_content);
_test_item_journal_link('customers',$customer);
_test_journal_options_head('customers',$customer->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('customers',$customer->{id},$customer,'create',$journals);
_test_journal_options_head('customers',$customer->{id},$journal->{id});
$req = HTTP::Request->new('PUT', $customer_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
status => "active",
contact_id => $customer_contact->{id},
type => "sipaccount",
billing_profile_id => $billing_profile->{id}, #$billing_profile_id,
max_subscribers => undef,
external_id => int(rand(10)),
}));
$res = $ua->request($req);
is($res->code, 200, "PUT test customer");
$req = HTTP::Request->new('GET', $customer_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PUT test customer");
$customer = JSON::from_json($res->decoded_content);
_test_item_journal_link('customers',$customer);
$journal = _test_journal_top_journalitem('customers',$customer->{id},$customer,'update',$journals,$journal);
$req = HTTP::Request->new('PATCH', $customer_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/status', value => 'pending' } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test customer");
$req = HTTP::Request->new('GET', $customer_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test customer");
$customer = JSON::from_json($res->decoded_content);
_test_item_journal_link('customers',$customer);
$journal = _test_journal_top_journalitem('customers',$customer->{id},$customer,'update',$journals,$journal);
_test_journal_collection('customers',$customer->{id},$journals);
return $customer;
}
sub test_subscriber {
my ($t,$customer,$domain) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/subscribers/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
domain_id => $domain->{id},
username => 'test_customer_subscriber_'.($t-1),
password => 'test_customer_subscriber_password',
customer_id => $customer->{id},
#primary_number
#status => "active",
#administrative
#is_pbx_pilot
#profile_set_id
#profile_id
#id
#alias_numbers => []
#customer_id - pbxaccount
#admin
#pbx_extension
#is_pbx_group
#pbx_group_ids => []
#display_name
#external_id
#preferences
#groups
#status => "active",
#contact_id => $customer_contact->{id},
#type => "sipaccount",
#billing_profile_id => $billing_profile->{id},
#max_subscribers => undef,
#external_id => undef,
}));
$res = $ua->request($req);
is($res->code, 201, "POST test subscriber");
my $subscriber_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $subscriber_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test subscriber");
my $subscriber = JSON::from_json($res->decoded_content);
_test_item_journal_link('subscribers',$subscriber);
_test_journal_options_head('subscribers',$subscriber->{id});
my $journals = {};
my $journal = _test_journal_top_journalitem('subscribers',$subscriber->{id},$subscriber,'create',$journals);
_test_journal_options_head('subscribers',$subscriber->{id},$journal->{id});
$req = HTTP::Request->new('PUT', $subscriber_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
domain_id => $domain->{id},
username => 'test_customer_subscriber_'.($t-1),
password => => 'test_customer_subscriber_password_PUT',
customer_id => $customer->{id},
}));
$res = $ua->request($req);
is($res->code, 200, "PUT test subscriber");
$req = HTTP::Request->new('GET', $subscriber_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PUT test subscriber");
$subscriber = JSON::from_json($res->decoded_content);
_test_item_journal_link('subscribers',$subscriber);
$journal = _test_journal_top_journalitem('subscribers',$subscriber->{id},$subscriber,'update',$journals,$journal);
$req = HTTP::Request->new('PATCH', $subscriber_uri);
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json(
[ { op => 'replace', path => '/password', value => 'test_customer_subscriber_password_PATCH', } ]
));
$res = $ua->request($req);
is($res->code, 200, "PATCH test subscriber");
$req = HTTP::Request->new('GET', $subscriber_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch PATCHed test subscriber");
$subscriber = JSON::from_json($res->decoded_content);
_test_item_journal_link('subscribers',$subscriber);
$journal = _test_journal_top_journalitem('subscribers',$subscriber->{id},$subscriber,'update',$journals,$journal);
$req = HTTP::Request->new('DELETE', $subscriber_uri);
$res = $ua->request($req);
is($res->code, 204, "delete POSTed test subscriber");
#$domain = JSON::from_json($res->decoded_content);
$journal = _test_journal_top_journalitem('subscribers',$subscriber->{id},$subscriber,'delete',$journals,$journal);
_test_journal_collection('subscribers',$subscriber->{id},$journals);
$req = HTTP::Request->new('POST', $uri.'/api/subscribers/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
domain_id => $domain->{id},
username => 'test_customer_subscriber_'.$t,
password => => 'test_customer_subscriber_password',
customer_id => $customer->{id},
}));
$res = $ua->request($req);
is($res->code, 201, "POST another test subscriber");
$subscriber_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $subscriber_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test subscriber");
$subscriber = JSON::from_json($res->decoded_content);
return $subscriber;
}
sub _test_item_journal_link {
my ($resource,$item) = @_;
if (_is_journal_resource_enabled($resource)) {
ok(exists $item->{_links}, "check existence of _links");
ok($item->{_links}->{'ngcp:journal'}, "check existence of ngcp:journal link");
ok($item->{_links}->{'ngcp:journal'}->{href} eq '/api/'.$resource . '/' . $item->{id} . '/journal/', "check if ngcp:journal link equals '/api/$resource/$item->{id}/journal/'");
}
}
sub _test_journal_top_journalitem {
my ($resource,$item_id,$content,$op,$journals,$old_journal) = @_;
if (_is_journal_resource_enabled($resource)) {
my $url = $uri.'/api/'.$resource . '/' . $item_id . '/journal/recent';
if (defined $op) {
$url .= '?operation=' . $op;
}
$req = HTTP::Request->new('GET',$url);
$res = $ua->request($req);
is($res->code, 200, "check recent '$op' journalitem request");
my $journal = JSON::from_json($res->decoded_content);
ok(exists $journal->{id}, "check existence of id");
ok(exists $journal->{operation}, "check existence of operation");
ok(exists $journal->{username}, "check existence of username");
ok(exists $journal->{timestamp}, "check existence of timestamp");
ok(exists $journal->{content}, "check existence of content");
ok(exists $journal->{_links}, "check existence of _links");
#ok(exists $journal->{_embedded}, "check existence of _embedded");
ok($journal->{_links}->{self}, "check existence of self link");
ok($journal->{_links}->{collection}, "check existence of collection link");
ok($journal->{_links}->{'ngcp:'.$resource}, "check existence of ngcp:$resource link");
ok($journal->{_links}->{'ngcp:'.$resource}->{href} eq '/api/'.$resource . '/' . $item_id, "check if ngcp:$resource link equals '/api/$resource/$item_id'");
if (defined $old_journal) {
ok($journal->{timestamp} ge $old_journal->{timestamp},"check incremented timestamp");
}
if (defined $content) {
my $original = Storable::dclone($content);
delete $original->{_links};
#delete $original->{_embedded};
is_deeply($journal->{content}, $original, "check resource '/api/$resource/$item_id' content deeply");
}
if (defined $journals) {
$journals->{$journal->{_links}->{self}->{href}} = $journal;
}
return $journal;
}
return undef;
}
sub _test_journal_options_head {
my ($resource,$item_id,$id) = @_;
if (_is_journal_resource_enabled($resource)) {
my $url = $uri.'/api/'.$resource . '/' . $item_id . '/journal/';
if (defined $id) {
$url .= $id . '/';
}
$req = HTTP::Request->new('OPTIONS', $url);
$res = $ua->request($req);
is($res->code, 200, "check journal options request");
#is($res->header('Accept-Post'), "application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-customers", "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 )) {
ok(grep(/^$opt$/, @hopts), "check for existence of '$opt' in Allow header");
ok(grep(/^$opt$/, @{ $opts->{methods} }), "check for existence of '$opt' in body");
}
$req = HTTP::Request->new('HEAD', $url);
$res = $ua->request($req);
is($res->code, 200, "check options request");
}
}
sub _test_journal_collection {
my ($resource,$item_id,$journals) = @_;
if (_is_journal_resource_enabled($resource)) {
my $total_count = (defined $journals ? (scalar keys %$journals) : undef);
my $nexturi = $uri.'/api/'.$resource . '/' . $item_id . '/journal/?page=1&rows=' . ((not defined $total_count or $total_count <= 2) ? 2 : $total_count - 1);
do {
$res = $ua->get($nexturi);
is($res->code, 200, "fetch journal collection page");
my $collection = JSON::from_json($res->decoded_content);
my $selfuri = $uri . $collection->{_links}->{self}->{href};
is($selfuri, $nexturi, "check _links.self.href of collection");
my $colluri = URI->new($selfuri);
ok(defined $total_count ? ($collection->{total_count} == $total_count) : ($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:journal'} eq "ARRAY", "check if 'ngcp:journal' is array");
my $page_journals = {};
foreach my $journal (@{ $collection->{_links}->{'ngcp:journal'} }) {
#delete $customers{$c->{href}};
ok(exists $journals->{$journal->{href}},"check page journal item link");
$req = HTTP::Request->new('GET',$uri . $journal->{href});
$res = $ua->request($req);
is($res->code, 200, "fetch page journal item");
my $original = delete $journals->{$journal->{href}};
$page_journals->{$original->{id}} = $original;
}
foreach my $journal (@{ $collection->{_embedded}->{'ngcp:journal'} }) {
ok(exists $page_journals->{$journal->{id}},"check existence of linked journal item among embedded");
my $original = delete $page_journals->{$journal->{id}};
delete $original->{content};
is_deeply($original,$journal,"compare created and embedded journal item deeply");
}
ok((scalar keys $page_journals) == 0,"check if all embedded journal items are linked");
} while($nexturi);
ok((scalar keys $journals) == 0,"check if journal collection lists all created journal items" . (defined $total_count ? " ($total_count)" : ''));
}
}
sub _is_journal_resource_enabled {
my ($resource) = @_;
my $cfg = NGCP::Panel::Utils::Journal::get_journal_resource_config(\%config,$resource);
if (not $cfg->{journal_resource_enabled}) {
diag("'api/$resource' journal disabled, skipping tests");
}
return ($enable_journal_tests && $cfg->{journal_resource_enabled});
}
# vim: set tabstop=4 expandtab:
Loading…
Cancel
Save