diff --git a/t/lib/NGCP/TestFramework.pm b/t/lib/NGCP/TestFramework.pm new file mode 100644 index 0000000000..61d440a6d5 --- /dev/null +++ b/t/lib/NGCP/TestFramework.pm @@ -0,0 +1,180 @@ +package NGCP::TestFramework; + +use strict; +use warnings; +use Cpanel::JSON::XS; +use Data::Walk; +use DateTime qw(); +use DateTime::Format::Strptime qw(); +use DateTime::Format::ISO8601 qw(); +use Digest::MD5 qw/md5_hex/; +use Moose; +use Net::Domain qw(hostfqdn); +use Test::More; +use URI; +use YAML::XS qw(LoadFile); +use Data::Dumper; + +use NGCP::TestFramework::RequestBuilder; +use NGCP::TestFramework::Client; +use NGCP::TestFramework::TestExecutor; + +has 'file_path' => ( + isa => 'Str', + is => 'ro' +); + +sub run { + my ( $self ) = @_; + + unless ( $self->file_path ) { + return; + } + $YAML::XS::DumpCode = 1; + $YAML::XS::LoadCode = 1; + $YAML::XS::UseCode = 1; + $YAML::XS::LoadBlessed = 1; + + my $testing_data = LoadFile($self->file_path); + + my $base_uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443'); + my $request_builder = NGCP::TestFramework::RequestBuilder->new({ base_uri => $base_uri }); + my $client = NGCP::TestFramework::Client->new( { uri => $base_uri, log_debug => 0 } ); + my $test_executor = NGCP::TestFramework::TestExecutor->new(); + + # initializing time to add to fields which need to be unique + my $retained = { unique_id => int(rand(100000)) }; + + foreach my $test ( @$testing_data ) { + next if ( $test->{skip} ); + + # build request + my $request = $request_builder->build({ + method => $test->{method}, + path => $test->{path}, + header => $test->{header} || undef, + content => $test->{content} || undef, + retain => $retained + }); + + # handle separate types + if ( $test->{type} eq 'item' ) { + my $result = $client->perform_request($request); + if ( $test->{retain} ) { + $self->_get_retained_elements( $test->{retain}, $retained, $result ); + } + if ( $test->{operations} ) { + $self->_perform_operations( $test->{operations}, $retained ); + } + if ( $test->{perl_code} ){ + my $sub = $test->{perl_code}; + warn Dumper $sub; + $sub->( $retained ); + } + $test_executor->run_tests( $test->{conditions}, $result, $retained, $test->{name} ) if ( $test->{conditions} ); + } + elsif ( $test->{type} eq 'batch' ) { + foreach my $iteration ( 1..$test->{iterations} ) { + my $result = $client->perform_request($request); + if ( $test->{retain} ) { + $self->_get_retained_elements( $test->{retain}, $retained, $result ); + } + if ( $test->{operations} ) { + $self->_perform_operations( $test->{operations}, $retained ); + } + $test_executor->run_tests( $test->{conditions}, $result, $retained, $test->{name} ) if ( $test->{conditions} ); + } + } + elsif ( $test->{type} eq 'pagination' ) { + my $nexturi = $test->{path}; + do { + $request->uri( $base_uri.$nexturi ); + my $result = $client->perform_request($request); + if ( $test->{retain} ) { + $self->_get_retained_elements( $test->{retain}, $retained, $result ); + } + if ( $test->{operations} ) { + $self->_perform_operations( $test->{operations}, $retained ); + } + my $body = decode_json( $result->decoded_content() ); + + #build default conditions for pagination + $test->{conditions}->{is}->{$nexturi} = $body->{_links}->{self}->{href}; + + my $colluri = URI->new($base_uri.$body->{_links}->{self}->{href}); + my %q = $colluri->query_form; + $test->{conditions}->{ok}->{$q{page}} = 'defined'; + $test->{conditions}->{ok}->{$q{rows}} = 'defined'; + my $page = int($q{page}); + my $rows = int($q{rows}); + if($page == 1) { + $test->{conditions}->{ok}->{'${collection}._links.prev.href'} = 'undefined'; + } else { + $test->{conditions}->{ok}->{'${collection}._links.prev.href'} = 'defined'; + } + if(($retained->{collection}->{total_count} / $rows) <= $page) { + $test->{conditions}->{ok}->{'${collection}._links.next.href'} = 'undefined'; + } else { + $test->{conditions}->{ok}->{'${collection}._links.next.href'} = 'defined'; + } + + $test_executor->run_tests( $test->{conditions}, $result, $retained, $test->{name} ) if ( $test->{conditions} ); + + if( $body->{_links}->{next}->{href} ) { + $nexturi = $body->{_links}->{next}->{href}; + } else { + $nexturi = undef; + } + } while ( $nexturi ) + } + } +} + +sub _get_retained_elements { + my ( $self, $retain, $retained, $result ) = @_; + + while ( my ( $retain_elem, $retain_value ) = each %{$retain} ) { + if ( $retain_value =~ /.+\..+/ ) { + my @splitted_values = split (/\./, $retain_value); + $retained->{$retain_elem} = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retain_elem ); + } + elsif ( $retain_value eq 'body' ) { + $retained->{$retain_elem} = decode_json( $result->decoded_content() ); + } + else { + return { + success => 0, + message => 'Wrong retain instructions!' + } + } + } +} + +sub _retrieve_from_composed_key { + my ( $self, $result, $splitted_values, $retain_elem ) = @_; + + if ( $splitted_values->[0] eq 'header' ) { + my $value = $result->header(ucfirst $splitted_values->[1]); + if ( $retain_elem =~ /^.+_id$/ ) { + $value =~ /^.+\/(\d+)$/; + $value = $1; + } + return $value; + } +} + +sub _variables_available { + my( $self, $retained, $test ) = @_; + + return 0 if ( $test->{path} =~ /\$\{(.*)\}/ && !$retained->{$1} ); + + # substitute variables in content + if ( $test->{content} && ref $test->{content} eq 'HASH' ) { + foreach my $content_key (keys %{$test->{content}}) { + return 0 if ( $test->{content}->{$content_key} =~ /\$\{(.*)\}/ && !$retained->{$1} ); + } + } + return 1; +} + +1; diff --git a/t/lib/NGCP/TestFramework/Client.pm b/t/lib/NGCP/TestFramework/Client.pm new file mode 100644 index 0000000000..c4d7ede393 --- /dev/null +++ b/t/lib/NGCP/TestFramework/Client.pm @@ -0,0 +1,191 @@ +package NGCP::TestFramework::Client; + +use strict; +use warnings; +use Data::Dumper; +use Digest::MD5 qw/md5_hex/; +use IO::Uncompress::Unzip; +use LWP::UserAgent; +use Moose; +use Time::HiRes qw/gettimeofday tv_interval/; + +has 'username' => ( + isa => 'Str', + is => 'ro', + default => sub { + $ENV{API_USER} // 'administrator'; + } +); + +has 'password' => ( + isa => 'Str', + is => 'ro', + default => sub { + $ENV{API_PASS} // 'administrator'; + } +); + +has 'role' => ( + isa => 'Str', + is => 'ro', + default => 'admin', +); + +has 'uri' => ( + isa => 'Str', + is => 'ro', + default => sub { $ENV{CATALYST_SERVER}; }, +); + +has 'sub_uri' => ( + isa => 'Str', + is => 'ro', + default => sub { $ENV{CATALYST_SERVER_SUB}; }, +); + +has '_uri' => ( + isa => 'Maybe[Str]', + is => 'rw', +); + +has 'verify_ssl' => ( + isa => 'Int', + is => 'ro', + default => 0, +); + +has 'log_debug' => ( + isa => 'Bool', + is => 'rw', + default => 0, +); + +has '_crt_path' => ( + is => 'ro', + isa => 'Str', + lazy => 1, + default => sub { + my ($self) = @_; + return '/tmp/' . md5_hex($self->username) . ".crt"; + } +); + +has 'last_rtt' => ( + is => 'rw', + isa => 'Num', + default => 0, +); + +has '_ua' => ( + isa => 'LWP::UserAgent', + is => 'ro', + lazy => 1, + builder => '_build_ua' +); + +sub _build_ua { + my $self = shift; + + my $ua = LWP::UserAgent->new(keep_alive => 1); + my $uri; + if($self->role eq "admin" || $self->role eq "reseller") { + $uri = $self->uri; + } else { + $uri = $self->sub_uri; + } + $self->_uri($uri); + $self->debug("client using uri $uri\n"); + $uri =~ s/^https?:\/\///; + $self->debug("client using ip:port $uri\n"); + my $realm; + if($self->role eq 'admin' || $self->role eq 'reseller') { + $realm = 'api_admin_http'; + } elsif($self->role eq 'subscriber') { + $realm = 'api_subscriber_http'; + } + $self->debug("client using realm $realm with user=" . $self->username . " and pass " . $self->password . "\n"); + $ua->credentials($uri, $realm, $self->username, $self->password); + unless($self->verify_ssl) { + $ua->ssl_opts( + verify_hostname => 0, + SSL_verify_mode => 0, + ); + } + if($self->role eq "admin" || $self->role eq "reseller") { + unless(-f $self->_crt_path) { + + # we have to setup a new connection here, because if we're already connected, + # the connection will be re-used, thus no cert is used + my $tmpua = LWP::UserAgent->new; + $tmpua->credentials($uri, $realm, $self->username, $self->password); + unless($self->verify_ssl) { + $tmpua->ssl_opts( + verify_hostname => 0, + SSL_verify_mode => 0, + ); + } + + my $res = $tmpua->post( + $self->_uri . '/api/admincerts/', + Content_Type => 'application/json', + Content => '{}' + ); + unless($res->is_success) { + die "Failed to fetch client certificate: " . $res->status_line . "\n"; + } + my $zip = $res->decoded_content; + my $z = IO::Uncompress::Unzip->new(\$zip, MultiStream => 0, Append => 1); + my $data; + while(!$z->eof() && (my $hdr = $z->getHeaderInfo())) { + unless($hdr->{Name} =~ /\.pem$/) { + # wrong file, just read stream, clear buffer and try next + while($z->read($data) > 0) {} + $data = undef; + $z->nextStream(); + next; + } + while($z->read($data) > 0) {} + last; + } + $z->close(); + unless($data) { + die "Failed to find PEM file in client certificate zip file\n"; + } + open my $fh, ">:raw", $self->_crt_path + or die "Failed to open " . $self->_crt_path . ": $!\n"; + print $fh $data; + close $fh; + } + $ua->ssl_opts( + SSL_cert_file => $self->_crt_path, + SSL_key_file => $self->_crt_path, + ); + } + return $ua; +} + + +sub perform_request { + my ($self, $req) = @_; + my $t0 = [gettimeofday]; + $self->debug("content of " . $req->method . " request to " . $req->uri . ":\n"); + $self->debug($req->content || "<empty request>"); + $self->debug("\n"); + my $res = $self->_ua->request($req); + my $rtt = tv_interval($t0); + $self->last_rtt($rtt); + $self->debug("content of response:\n"); + $self->debug($res->decoded_content || "<empty response>"); + $self->debug("\n"); + return $res; +} + +sub debug { + my ($self, $msg) = @_; + if($self->log_debug) { + print $msg; + } +} + +1; + diff --git a/t/lib/NGCP/TestFramework/Interface/BillingNetworks.yaml b/t/lib/NGCP/TestFramework/Interface/BillingNetworks.yaml new file mode 100644 index 0000000000..86121e455e --- /dev/null +++ b/t/lib/NGCP/TestFramework/Interface/BillingNetworks.yaml @@ -0,0 +1,655 @@ +--- +#check options +- + name: check OPTIONS for billingnetworks + type: item + method: OPTIONS + path: /api/billingnetworks/ + conditions: + is: + code: 200 + header: + Accept-Post: application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-billingnetworks + ok: + options: + - GET + - HEAD + - OPTIONS + - POST + perl_code: !!perl/code | + { + my ($retained) = @_; + $retained->{blocks} = [{ip=>'fdfe::5a55:caff:fefa:9089',mask=>128}, + {ip=>'fdfe::5a55:caff:fefa:908a'}, + {ip=>'fdfe::5a55:caff:fefa:908b',mask=>128},]; + } + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: + name: test billing network ${unique_id} + description: test billing network description ${unique_id} + reseller_id: 1 + blocks: ${blocks} + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork: body + conditions: + is: + code: 200 + +#PUT test billingnetwork +- + name: PUT test billingnetwork + type: item + method: PUT + path: '/${billingnetwork_path}' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: test billingnetwork PUT ${unique_id} + description: test billing network description PUT ${unique_id} + reseller_id: 1 + blocks: ${blocks} + conditions: + is: + code: 200 + +#GET billingnetwork +- + name: fetch PUT test billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork: body + conditions: + is: + code: 200 + +#PATCH test billingnetwork +- + name: PATCH test billingnetwork + type: item + method: PATCH + path: '/${billingnetwork_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /name + value: test billingnetwork PATCH ${unique_id} + conditions: + is: + code: 200 + +#GET billingnetwork +- + name: fetch PATCHed test billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork: body + conditions: + is: + code: 200 + +#DELETE billingnetwork +- + name: terminate test billingnetwork + type: item + method: DELETE + path: '${billingnetwork_path}' + conditions: + is: + code: 204 + +#GET billingnetwork +- + name: try to fetch terminated billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork: body + conditions: + is: + code: 404 + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv6 billing network 1 ${unique_id} + description: test ipv6 billing network description 1 ${unique_id} + reseller_id: 1 + blocks: + - + ip: 'fdfe::5a55:caff:fefa:9089' + mask: 128 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv6_1: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv6_1}->{id}; + delete $retained->{billingnetwork_ipv6_1}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv6_1}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv6 billing network 2 ${unique_id} + description: test ipv6 billing network description 2 ${unique_id} + reseller_id: 1 + blocks: + - + ip: 'fdfe::5a55:caff:fefa:908a' + mask: null + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv6_2: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv6_2}->{id}; + delete $retained->{billingnetwork_ipv6_2}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv6_2}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv6 billing network 3 ${unique_id} + description: test ipv6 billing network description 3 ${unique_id} + reseller_id: 1 + blocks: + - + ip: 'fdfe::5a55:caff:fefa:908b' + mask: 128 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv6_3: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv6_3}->{id}; + delete $retained->{billingnetwork_ipv6_3}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv6_3}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv6 billing network 4 ${unique_id} + description: test ipv6 billing network description 4 ${unique_id} + reseller_id: 1 + blocks: + - + ip: 'fdfe::5a55:caff:fefa:9089' + mask: 128 + - + ip: 'fdfe::5a55:caff:fefa:908a' + mask: null + - + ip: 'fdfe::5a55:caff:fefa:908b' + mask: 128 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv6_4: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv6_4}->{id}; + delete $retained->{billingnetwork_ipv6_4}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv6_4}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv4 billing network 1 ${unique_id} + description: test ipv4 billing network description 1 ${unique_id} + reseller_id: 1 + blocks: + - + ip: '10.0.4.7' + mask: 26 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv4_1: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv4_1}->{id}; + delete $retained->{billingnetwork_ipv4_1}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv4_1}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv4 billing network 2 ${unique_id} + description: test ipv4 billing network description 2 ${unique_id} + reseller_id: 1 + blocks: + - + ip: '10.0.4.99' + mask: 26 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv4_2: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv4_2}->{id}; + delete $retained->{billingnetwork_ipv4_2}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv4_2}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv4 billing network 3 ${unique_id} + description: test ipv4 billing network description 3 ${unique_id} + reseller_id: 1 + blocks: + - + ip: '10.0.5.9' + mask: 24 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv4_3: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv4_3}->{id}; + delete $retained->{billingnetwork_ipv4_3}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv4_3}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv4 billing network 4 ${unique_id} + description: test ipv4 billing network description 4 ${unique_id} + reseller_id: 1 + blocks: + - + ip: '10.0.6.9' + mask: 24 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv4_4: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv4_4}->{id}; + delete $retained->{billingnetwork_ipv4_4}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv4_4}': *content + +#POST test billingnetwork +- + name: POST test billingnetwork + type: item + method: POST + path: /api/billingnetworks/ + header: + Content-Type: application/json + Prefer: return=representation + content: &content + name: test ipv4 billing network 5 ${unique_id} + description: test ipv4 billing network description 5 ${unique_id} + reseller_id: 1 + blocks: + - + ip: '10.0.4.7' + mask: 26 + - + ip: '10.0.4.99' + mask: 26 + - + ip: '10.0.5.9' + mask: 24 + - + ip: '10.0.6.9' + mask: 24 + status: active + retain: + billingnetwork_path: header.location + conditions: + is: + code: 201 + +#GET billingnetwork +- + name: fetch POSTed billingnetwork + type: item + method: GET + path: '/${billingnetwork_path}' + retain: + billingnetwork_ipv4_5: body + perl_code: !!perl/code | + { + my ($retained) = @_; + delete $retained->{billingnetwork_ipv4_5}->{id}; + delete $retained->{billingnetwork_ipv4_5}->{_links}; + } + conditions: + is: + code: 200 + is_deeply: + '${billingnetwork_ipv4_5}': *content + +#compare filtered collection +- + name: compare filtered collection + type: item + method: GET + path: '/api/billingnetworks/?page=1&rows=5&ip=fdfe::5a55:caff:fefa:9089&name=%25${unique_id}' + retain: + collection: body + perl_code: !!perl/code | + { + my ($retained) = @_; + $retained->{expected} = [ $retained->{billingnetwork_ipv6_1}, $retained->{billingnetwork_ipv6_4} ]; + map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + } + conditions: + is: + code: 200 + is_deeply: + '${collection}._embedded.ngcp:billingnetworks': ${expected} + +#compare filtered collection +- + name: compare filtered collection + type: item + method: GET + path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.4.0&name=%25${unique_id}' + retain: + collection: body + perl_code: !!perl/code | + { + my ($retained) = @_; + $retained->{expected} = [ $retained->{billingnetwork_ipv4_1}, $retained->{billingnetwork_ipv4_5} ]; + map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + } + conditions: + is: + code: 200 + is_deeply: + '${collection}._embedded.ngcp:billingnetworks': ${expected} + +#compare filtered collection +- + name: compare filtered collection + type: item + method: GET + path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.4.64&name=%25${unique_id}' + retain: + collection: body + perl_code: !!perl/code | + { + my ($retained) = @_; + $retained->{expected} = [ $retained->{billingnetwork_ipv4_2}, $retained->{billingnetwork_ipv4_5} ]; + map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + } + conditions: + is: + code: 200 + is_deeply: + '${collection}._embedded.ngcp:billingnetworks': ${expected} + +#compare filtered collection +- + name: compare filtered collection + type: item + method: GET + path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.5.255&name=%25${unique_id}' + retain: + collection: body + perl_code: !!perl/code | + { + my ($retained) = @_; + $retained->{expected} = [ $retained->{billingnetwork_ipv4_3}, $retained->{billingnetwork_ipv4_5} ]; + map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + } + conditions: + is: + code: 200 + is_deeply: + '${collection}._embedded.ngcp:billingnetworks': ${expected} + +#compare filtered collection +- + name: compare filtered collection + type: item + method: GET + path: '/api/billingnetworks/?page=1&rows=5&ip=10.0.6.255&name=%25${unique_id}' + retain: + collection: body + perl_code: !!perl/code | + { + my ($retained) = @_; + $retained->{expected} = [ $retained->{billingnetwork_ipv4_4}, $retained->{billingnetwork_ipv4_5} ]; + map { delete $_->{id} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + map { delete $_->{_links} } @{$retained->{collection}->{'_embedded'}->{'ngcp:billingnetworks'}}; + } + conditions: + is: + code: 200 + is_deeply: + '${collection}._embedded.ngcp:billingnetworks': ${expected} \ No newline at end of file diff --git a/t/lib/NGCP/TestFramework/Interface/Calls.yaml b/t/lib/NGCP/TestFramework/Interface/Calls.yaml new file mode 100644 index 0000000000..52bc5f7569 --- /dev/null +++ b/t/lib/NGCP/TestFramework/Interface/Calls.yaml @@ -0,0 +1,35 @@ +--- +#get a subscriber for testing +- + name: get a subscriber for testing + type: item + method: GET + path: '/api/subscribers/?page=1&rows=1&order_by=id&order_by_direction=desc' + retain: + subscriber: body + perl_code: !!perl/code | + { + my ($retained) = @_; + my $subscriber = $retained->{subscriber}->{'_embedded'}->{'ngcp:subscribers'}->[0]; + $retained->{subscriber} = $subscriber; + $retained->{subscriber_id} = $subscriber->{id}; + } + conditions: + is: + code: 200 + ok: + '${subscriber}.id': defined + +#fetch calls for subscriber +- + name: get a subscriber for testing + type: item + method: GET + path: '/api/calls/?page=1&rows=10&subscriber_id=${subscriber_id}' + retain: + calls: body + conditions: + is: + code: 200 + ok: + '${calls}.total_count': defined \ No newline at end of file diff --git a/t/lib/NGCP/TestFramework/Interface/Contracts.yaml b/t/lib/NGCP/TestFramework/Interface/Contracts.yaml new file mode 100644 index 0000000000..e7d0c01f8d --- /dev/null +++ b/t/lib/NGCP/TestFramework/Interface/Contracts.yaml @@ -0,0 +1,1015 @@ +--- +#check options +- + name: check OPTIONS for contracts + type: item + method: OPTIONS + path: /api/contracts/ + conditions: + is: + code: 200 + header: + Accept-Post: application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-contracts + ok: + options: + - GET + - HEAD + - OPTIONS + - POST + +#create a BillingProfile +- + name: create a BillingProfile + type: item + thread: 1 + method: POST + path: /api/billingprofiles/ + header: + Content-Type: application/json + Prefer: return=representation + content: + name: test profile ${unique_id} + handle: test_profile_handle${unique_id} + reseller_id: 1 + conditions: + is: + code: 201 + retain: + billing_profile_id: header.location + +#create a Customer Contact +- + name: create a Customer Contact + type: item + method: POST + path: /api/customercontacts/ + header: + Content-Type: application/json + content: + firstname: cust_contact_first + lastname: cust_contact_last + email: cust_contact@custcontact.invalid + reseller_id: 1 + conditions: + is: + code: 201 + retain: + customer_contact_path: header.location + customer_contact_id: header.location + +#check CustomerContact +- + name: check CustomerContact + type: item + method: GET + path: '/${customer_contact_path}' + conditions: + is: + code: 200 + +#create a System Contact +- + name: create a System Contact + type: item + method: POST + path: /api/systemcontacts/ + header: + Content-Type: application/json + content: + firstname: sys_contact_first + lastname: sys_contact_last + email: sys_contact@syscontact.invalid + conditions: + is: + code: 201 + retain: + system_contact_path: header.location + system_contact_id: header.location + +#get System Contact +- + name: get System Contact + type: item + method: GET + path: '/${system_contact_path}' + retain: + system_contact: body + +#create batch +- + name: create batch + type: batch + method: POST + path: '/api/contracts/' + iterations: 6 + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + type: reseller + billing_profile_id: ${billing_profile_id} + retain: + contract_path: header.location + conditions: + is: + code: 201 + +#create invalid Contract with wrong type +- + name: create invalid Contract with wrong type + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + billing_profile_id: ${billing_profile_id} + type: invalid + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: Validation failed.*type + +#create invalid Contract with wrong billing profile +- + name: create invalid Contract with wrong billing profile + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + billing_profile_id: 999999 + type: reseller + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: Invalid 'billing_profile_id' + +#create invalid Contract with customercontact +- + name: create invalid Contract with customercontact + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + type: reseller + billing_profile_id: ${billing_profile_id} + contact_id: ${customer_contact_id} + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: The contact_id is not a valid ngcp:systemcontacts item + +#create invalid Contract without contact +- + name: create invalid Contract without contact + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + type: reseller + billing_profile_id: ${billing_profile_id} + conditions: + is: + code: 422 + +#create invalid Contract with invalid status +- + name: create invalid Contract with invalid status + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: invalid + type: reseller + billing_profile_id: ${billing_profile_id} + contact_id: ${system_contact_id} + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: field='status' + +#verify pagination +- + name: verify pagination + skip: 1 + type: pagination + method: GET + path: '/api/contracts/?page=1&rows=5' + retain: + collection: body + conditions: + is: + code: 200 + +#check options on contract +- + name: check OPTIONS on contract + type: item + method: OPTIONS + path: '/${contract_path}' + conditions: + is: + code: 200 + ok: + options: + - GET + - HEAD + - OPTIONS + - PUT + - PATCH + +#get contract +- + name: GET contract + type: item + method: GET + path: '/${contract_path}' + retain: + contract: body + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{contract}->{all_billing_profiles}}; + } + conditions: + is: + code: 200 + ok: + '${contract}.status': defined + '${contract}.type': defined + '${contract}.all_billing_profiles': defined + like: + '${contract}.billing_profile_id': '[0-9]+' + '${contract}.contact_id': '[0-9]+' + '${contract}.id': '[0-9]+' + is_deeply: + '${contract}.all_billing_profiles': + - + profile_id: ${billing_profile_id} + start: null + stop: null + +#put contract with missing content-type +- + name: PUT contract with missing content-type + type: item + method: PUT + path: '/${contract_path}' + header: + Prefer: return=minimal + conditions: + is: + code: 415 + +#put contract with unsupported content type +- + name: PUT contract with unsupported Content-Type + type: item + method: PUT + path: '/${contract_path}' + header: + Content-Type: application/xxx + conditions: + is: + code: 415 + +#put contract with missing body +- + name: PUT contract with missing body + type: item + method: PUT + path: '/${contract_path}' + header: + Content-Type: application/json + Prefer: return=representation + conditions: + is: + code: 400 + +#put contract +- + name: PUT contract + type: item + method: PUT + path: '/${contract_path}' + header: + Content-Type: application/json + Prefer: return=representation + content: '${contract}' + retain: + new_contract: body + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{new_contract}->{all_billing_profiles}}; + } + conditions: + is: + code: 200 + is_deeply: + '${contract}': ${new_contract} + ok: + '${new_contract}._links.ngcp:systemcontacts': defined + '${new_contract}._links.ngcp:billingprofiles': defined + +#modify contract status +- + name: modify contract status + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: pending + retain: + modified_contract: body + conditions: + is: + code: 200 + '${modified_contract}.status': pending + '${modified_contract}._links.self.href': ${contract_path} + '${modified_contract}._links.collection.href': /api/contracts/ + +#check patch with status undef +- + name: check patch with status undef + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: null + conditions: + is: + code: 422 + +#check patch with invalid status +- + name: check patch with invalid status + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: invalid + conditions: + is: + code: 422 + +#check patch with invalid contact_id +- + name: check patch with invalid contact_id + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /contact_id + value: 99999 + conditions: + is: + code: 422 + +#check patch with customer contact_id +- + name: check patch with customer contact_id + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /contact_id + value: ${customer_contact_id} + conditions: + is: + code: 422 + +#check patch with undef billing_profile_id +- + name: check patch with undef billing_profile_id + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profile_id + value: null + conditions: + is: + code: 422 + +#check patch with invalid billing_profile_id +- + name: check patch with invalid billing_profile_id + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profile_id + value: 99999 + conditions: + is: + code: 422 + +#multi-bill-prof: create another test billing profile +- + name: 'multi-bill-prof: create another test billing profile' + type: item + method: POST + path: '/api/billingprofiles/' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: SECOND test profile ${unique_id} + handle: second_testprofile${unique_id} + reseller_id: 1 + retain: + second_billing_profile_id: header.location + conditions: + is: + code: 201 + perl_code: !!perl/code | + { + my ( $retained ) = @_; + + my $dtf = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); #DateTime::Format::Strptime->new( pattern => '%Y-%m-%d %H:%M:%S' ); + my $now = DateTime->now( + time_zone => DateTime::TimeZone->new(name => 'local') + ); + my $t1 = $now->clone->add(days => 1); + my $t2 = $now->clone->add(days => 2); + my $t3 = $now->clone->add(days => 3); + + my $billing_profile_id = $retained->{billing_profile_id}; + + $retained->{malformed_profilemappings1} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($now), + stop => $dtf->format_datetime($now),} ]; + $retained->{malformed_profilemappings2} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t1),} ]; + $retained->{malformed_profilemappings3} = [ { profile_id => $billing_profile_id, + start => undef, + stop => $dtf->format_datetime($now),},]; + $retained->{malformed_profilemappings4} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t2),}, ] , []; + $retained->{correct_profile_mappings1} = [ { profile_id => $retained->{second_billing_profile_id}, + start => undef, + stop => undef, }, + { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t2), }, + { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t2), + stop => $dtf->format_datetime($t3), } + ]; + $retained->{correct_profile_mappings2} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t2), }, + { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t2), + stop => $dtf->format_datetime($t3), }, + { profile_id => $retained->{second_billing_profile_id}, + start => $dtf->format_datetime($t3), + stop => undef, } + ]; + } + +#multi-bill-prof POST: check 'start' timestamp is not in future +- + name: 'multi-bill-prof POST: check "start" timestamp is not in future' + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + type: reseller + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings1} + conditions: + is: + code: 422 + +#multi-bill-prof POST: check 'start' timestamp has to be before 'stop' timestamp +- + name: 'multi-bill-prof POST: check "start" timestamp has to be before "stop" timestamp' + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + type: reseller + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings2} + conditions: + is: + code: 422 + +#multi-bill-prof POST: check Interval with 'stop' timestamp but no 'start' timestamp specified +- + name: 'multi-bill-prof POST: check "Interval with "stop" timestamp but no "start" timestamp specified"' + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + type: reseller + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings3} + conditions: + is: + code: 422 + +#multi-bill-prof POST: check An initial interval without 'start' and 'stop' timestamps is required +- + name: 'multi-bill-prof POST: check An initial interval without "start" and "stop" timestamps is required' + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + type: reseller + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings4} + conditions: + is: + code: 422 + +#multi-bill-prof: create test contract +- + name: 'multi-bill-prof: create test contract' + type: item + method: POST + path: '/api/contracts/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${system_contact_id} + type: reseller + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${correct_profile_mappings1} + retain: + contract_path: header.location + conditions: + is: + code: 201 + +#get contract +- + name: GET contract + type: item + method: GET + path: '/${contract_path}' + retain: + contract: body + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{contract}->{all_billing_profiles}}; + $retained->{malformed_profilemappings4} = [ { profile_id => $retained->{billing_profile_id}, + start => undef, + stop => undef,}, ]; + } + conditions: + is: + code: 200 + '${contract}.billing_profile_id': ${second_billing_profile_id} + ok: + '${contract}.profile_package_id': undefined + '${contract}.billing_profile_id': defined + '${contract}.billing_profiles': defined + '${contract}.all_billing_profiles': defined + is_deeply: + '${contract}.all_billing_profiles': ${correct_profile_mappings1} + +#multi-bill-prof PATCH: check 'start' timestamp is not in future +- + name: 'multi-bill-prof PATCH: check "start" timestamp is not in future' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings1} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: check 'start' timestamp has to be before 'stop' timestamp +- + name: 'multi-bill-prof PATCH: check "start" timestamp has to be before "stop" timestamp' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings2} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: check Interval with 'stop' timestamp but no 'start' timestamp specified +- + name: 'multi-bill-prof PATCH: check Interval with "stop" timestamp but no "start" timestamp specified' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings3} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: check Adding intervals without 'start' and 'stop' timestamps is not allowed. +- + name: 'multi-bill-prof PATCH: check Adding intervals without "start" and "stop" timestamps is not allowed.' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings4} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: test if patching profile_package_id fails +- + name: 'multi-bill-prof PATCH: test if patching profile_package_id fails' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /profile_package_id + value: null + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: test contract with new billing profile +- + name: 'multi-bill-prof PATCH: test contract with new billing profile' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + retain: + patched_contract: body + content: + - + op: replace + path: /billing_profile_id + value: ${billing_profile_id} + perl_code: !!perl/code | + { + my ($retained) = @_; + + $retained->{posted_profiles_number} = scalar @{$retained->{correct_profile_mappings1}} + 1; + $retained->{actual_profiles_number} = scalar @{$retained->{patched_contract}->{all_billing_profiles}}; + + my $now = DateTime->now( + time_zone => DateTime::TimeZone->new(name => 'local') + ); + foreach my $m ( @{$retained->{patched_contract}->{billing_profiles}} ) { + if (!defined $m->{start}) { + push(@{$retained->{expected_mappings}},$m); + next; + } + my $s = $m->{start}; + $s =~ s/^(\d{4}\-\d{2}\-\d{2})\s+(\d.+)$/$1T$2/; + my $start = DateTime::Format::ISO8601->parse_datetime($s); + $start->set_time_zone( DateTime::TimeZone->new(name => 'local') ); + push(@{$retained->{expected_mappings}},$m) if ($start <= $now); + } + push ( @{$retained->{expected_mappings}}, @{$retained->{correct_profile_mappings2}} ); + } + conditions: + is: + code: 200 + '${patched_contract}.billing_profile_id': ${billing_profile_id} + '${posted_profiles_number}': '${actual_profiles_number}' + ok: + '${patched_contract}.profile_package_id': undefined + '${patched_contract}.billing_profile_id': defined + '${patched_contract}.billing_profiles': defined + '${patched_contract}.all_billing_profiles': defined + +#multi-bill-prof: patch test contract +- + name: 'multi-bill-prof: patch test contract' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + retain: + patched_contract: body + content: + - + op: replace + path: /billing_profiles + value: ${correct_profile_mappings2} + conditions: + is: + code: 200 + +#get contract +- + name: GET contract + type: item + method: GET + path: '/${contract_path}' + retain: + got_contract: body + conditions: + is: + code: 200 + '${patched_contract}.billing_profile_id': ${billing_profile_id} + ok: + '${patched_contract}.billing_profile_id': defined + '${patched_contract}.billing_profiles': defined + is_deeply: + '${got_contract}': '${patched_contract}' + +#multi-bill-prof: put test contract +- + name: 'multi-bill-prof: put test contract' + type: item + method: PUT + path: '/${contract_path}' + header: + Content-Type: application/json + Prefer: return=representation + retain: + updated_contract: body + content: + status: active + contact_id: ${system_contact_id} + type: reseller + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${correct_profile_mappings2} + conditions: + is: + code: 200 + +#get contract +- + name: GET contract + type: item + method: GET + path: '/${contract_path}' + retain: + got_contract: body + conditions: + is: + code: 200 + '${updated_contract}.billing_profile_id': ${billing_profile_id} + ok: + '${updated_contract}.billing_profile_id': defined + '${updated_contract}.billing_profiles': defined + is_deeply: + '${got_contract}': '${updated_contract}' + + +#try to delete contact before terminating contracts +- + name: try to delete contact before terminating contracts + type: item + method: DELETE + path: '/api/systemcontacts/${system_contact_id}' + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{patched_contract}->{billing_profiles}}; + map { delete $_->{effective_start_time}; $_; } @{$retained->{updated_contract}->{billing_profiles}}; + } + conditions: + is: + code: 423 + is_deeply: + #perform tests against stripped mapping here, because deleting start_time would have affected previous is_deeply verification + '${patched_contract}.billing_profiles': '${expected_mappings}' + '${updated_contract}.billing_profiles': '${expected_mappings}' + +#multi-bill-prof: terminate contract +- + name: 'multi-bill-prof: terminate contract' + type: item + method: PATCH + path: '/${contract_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 200 + +#try to get already terminated contract +- + name: try to get already terminated contract + type: item + method: GET + path: '/${contract_path}' + conditions: + is: + code: 404 + +#terminate billingprofile +- + name: 'terminate billingprofile' + type: item + method: PATCH + path: '/api/billingprofiles/${billing_profile_id}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 200 + +#get contract 1 +- + name: 'get contract 1' + type: item + method: GET + path: '/api/contracts/1' + retain: + contract1: body + conditions: + is: + code: 200 + '${contract1}.id': 1 + '${contract1}.type': reseller + +#check contract 1 can't be terminated +- + name: 'check contract 1 can not be terminated' + type: item + method: PATCH + path: '/api/contracts/1' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 403 + +#get contract 1 again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits +- + name: 'get contract 1 again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits' + type: item + method: GET + path: '/api/contracts/1' + retain: + contract1: body + conditions: + is: + code: 200 + '${contract1}.id': 1 + '${contract1}.type': reseller + +#check contract 1 can't be terminated again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits +- + name: 'check contract 1 can not be terminated again to verify billing.schedule_contract_billing_profile_network proc contains no implicit commits' + type: item + method: PATCH + path: '/api/contracts/1' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 403 diff --git a/t/lib/NGCP/TestFramework/Interface/Customers.yaml b/t/lib/NGCP/TestFramework/Interface/Customers.yaml new file mode 100644 index 0000000000..e647fe6d73 --- /dev/null +++ b/t/lib/NGCP/TestFramework/Interface/Customers.yaml @@ -0,0 +1,1374 @@ +--- +#check options +- + name: check OPTIONS for customers + type: item + method: OPTIONS + path: /api/customers/ + conditions: + is: + code: 200 + header: + Accept-Post: application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-customers + ok: + options: + - GET + - HEAD + - OPTIONS + - POST + +#create a BillingProfile +- + name: create a BillingProfile + type: item + thread: 1 + method: POST + path: /api/billingprofiles/ + header: + Content-Type: application/json + Prefer: return=representation + content: + name: test profile ${unique_id} + handle: test_profile_handle${unique_id} + reseller_id: 1 + conditions: + is: + code: 201 + retain: + billing_profile_id: header.location + +#create a Customer Contact +- + name: create a Customer Contact + type: item + method: POST + path: /api/customercontacts/ + header: + Content-Type: application/json + content: + firstname: cust_contact_first + lastname: cust_contact_last + email: cust_contact@custcontact.invalid + reseller_id: 1 + conditions: + is: + code: 201 + retain: + customer_contact_path: header.location + customer_contact_id: header.location + +#check CustomerContact +- + name: check CustomerContact + type: item + method: GET + path: '/${customer_contact_path}' + conditions: + is: + code: 200 + +#create a System Contact +- + name: create a System Contact + type: item + method: POST + path: /api/systemcontacts/ + header: + Content-Type: application/json + content: + firstname: sys_contact_first + lastname: sys_contact_last + email: sys_contact@syscontact.invalid + conditions: + is: + code: 201 + retain: + system_contact_path: header.location + system_contact_id: header.location + +#get System Contact +- + name: get System Contact + type: item + method: GET + path: '/${system_contact_path}' + retain: + system_contact: body + +#create batch +- + name: create batch + type: batch + method: POST + path: '/api/customers/' + iterations: 6 + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + billing_profile_id: ${billing_profile_id} + max_subscribers: null + external_id: null + retain: + customer_path: header.location + conditions: + is: + code: 201 + +#create invalid Customer with wrong type +- + name: create invalid Customer with wrong type + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + billing_profile_id: ${billing_profile_id} + type: invalid + max_subscribers: null + external_id: null + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: is not a valid value + +#create invalid Customer with wrong billing profile +- + name: create invalid Customer with wrong billing profile + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + billing_profile_id: 999999 + type: sipaccount + max_subscribers: null + external_id: null + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: Invalid 'billing_profile_id' + +#create invalid Customer with systemcontact +- + name: create invalid Customer with systemcontact + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + type: sipaccount + billing_profile_id: ${billing_profile_id} + contact_id: ${system_contact_id} + max_subscribers: null + external_id: null + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: The contact_id is not a valid ngcp:customercontacts item + +#create invalid Customer without contact +- + name: create invalid Customer without contact + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + type: sipaccount + billing_profile_id: ${billing_profile_id} + max_subscribers: null + external_id: null + conditions: + is: + code: 422 + +#create invalid Customer with invalid status +- + name: create invalid Customer with invalid status + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: invalid + type: sipaccount + billing_profile_id: ${billing_profile_id} + contact_id: ${customer_contact_id} + max_subscribers: null + external_id: null + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: field='status' + +#create invalid Customer with invalid max_subscribers +- + name: create invalid Customer with invalid status + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + type: sipaccount + billing_profile_id: ${billing_profile_id} + contact_id: ${customer_contact_id} + max_subscribers: 'abc' + external_id: null + conditions: + is: + code: 422 + body.code: 422 + like: + body.message: field='max_subscribers' + +#verify pagination +- + skip: 1 + name: verify pagination + type: pagination + method: GET + path: '/api/customers/?page=1&rows=5' + retain: + collection: body + conditions: + is: + code: 200 + +#check options on customer +- + name: check OPTIONS on customer + type: item + method: OPTIONS + path: '/${customer_path}' + conditions: + is: + code: 200 + ok: + options: + - GET + - HEAD + - OPTIONS + - PUT + - PATCH + +#get customer +- + name: GET customer + type: item + method: GET + path: '/${customer_path}' + retain: + customer: body + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{customer}->{all_billing_profiles}}; + } + conditions: + is: + code: 200 + ok: + '${customer}.status': defined + '${customer}.type': defined + '${customer}.billing_profile_id': defined + '${customer}.contact_id': defined + '${customer}.id': defined + '${customer}.all_billing_profiles': defined + '${customer}.product_id': undefined + like: + '${customer}.billing_profile_id': '[0-9]+' + '${customer}.contact_id': '[0-9]+' + '${customer}.id': '[0-9]+' + is_deeply: + '${customer}.all_billing_profiles': + - + profile_id: ${billing_profile_id} + start: null + stop: null + network_id: null + +#put customer with missing content-type +- + name: PUT customer with missing content-type + type: item + method: PUT + path: '/${customer_path}' + header: + Prefer: return=minimal + conditions: + is: + code: 415 + +#put customer with unsupported content type +- + name: PUT customer with unsupported Content-Type + type: item + method: PUT + path: '/${customer_path}' + header: + Content-Type: application/xxx + conditions: + is: + code: 415 + +#put customer with missing body +- + name: PUT customer with missing body + type: item + method: PUT + path: '/${customer_path}' + header: + Content-Type: application/json + Prefer: return=representation + conditions: + is: + code: 400 + +#put customer +- + name: PUT customer + type: item + method: PUT + path: '/${customer_path}' + header: + Content-Type: application/json + Prefer: return=representation + content: '${customer}' + retain: + new_customer: body + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{new_customer}->{all_billing_profiles}}; + delete $retained->{customer}->{modify_timestamp}; + delete $retained->{new_customer}->{modify_timestamp}; + } + conditions: + is: + code: 200 + is_deeply: + '${customer}': ${new_customer} + ok: + '${new_customer}._links.ngcp:customercontacts': defined + '${new_customer}._links.ngcp:billingprofiles': defined + +#modify customer status +- + name: modify customer status + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: pending + retain: + modified_customer: body + conditions: + is: + code: 200 + '${modified_customer}.status': pending + '${modified_customer}._links.self.href': ${customer_path} + '${modified_customer}._links.collection.href': /api/customers/ + +#check patch with status undef +- + name: check patch with status undef + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: null + conditions: + is: + code: 422 + +#check patch with invalid status +- + name: check patch with invalid status + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: invalid + conditions: + is: + code: 422 + +#check patch with invalid contact_id +- + name: check patch with invalid contact_id + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /contact_id + value: 99999 + conditions: + is: + code: 422 + +#check patch with system contact_id +- + name: check patch with system contact_id + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /contact_id + value: ${system_contact_id} + conditions: + is: + code: 422 + +#check patch with undef billing_profile_id +- + name: check patch with undef billing_profile_id + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profile_id + value: null + conditions: + is: + code: 422 + +#check patch with invalid billing_profile_id +- + name: check patch with invalid billing_profile_id + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profile_id + value: 99999 + conditions: + is: + code: 422 + +#check patch with invalid max_subscribers +- + name: check patch with invalid billing_profile_id + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /max_subscribers + value: 'abc' + conditions: + is: + code: 422 + +#multi-bill-prof: create another test billing profile +- + name: 'multi-bill-prof: create another test billing profile' + type: item + method: POST + path: '/api/billingprofiles/' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: SECOND test profile ${unique_id} + handle: second_testprofile${unique_id} + reseller_id: 1 + retain: + second_billing_profile_id: header.location + conditions: + is: + code: 201 + +#multi-bill-prof: create test billing network +- + name: 'multi-bill-prof: create test billing network' + type: item + method: POST + path: '/api/billingnetworks/' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: test billing network ${unique_id} + description: test billing network description ${unique_id} + reseller_id: 1 + blocks: + - + ip: '10.0.4.7' + mask: '26' + - + ip: '10.0.4.99' + mask: '26' + - + ip: '10.0.5.9' + mask: '24' + - + ip: '10.0.6.9' + mask: '24' + retain: + billing_network_id: header.location + conditions: + is: + code: 201 + perl_code: !!perl/code | + { + my ( $retained ) = @_; + + my $dtf = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); #DateTime::Format::Strptime->new( pattern => '%Y-%m-%d %H:%M:%S' ); + my $now = DateTime->now( + time_zone => DateTime::TimeZone->new(name => 'local') + ); + my $t1 = $now->clone->add(days => 1); + my $t2 = $now->clone->add(days => 2); + my $t3 = $now->clone->add(days => 3); + + my $billing_profile_id = $retained->{billing_profile_id}; + + $retained->{malformed_profilemappings1} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($now), + stop => $dtf->format_datetime($now),} ]; + $retained->{malformed_profilemappings2} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t1),} ]; + $retained->{malformed_profilemappings3} = [ { profile_id => $billing_profile_id, + start => undef, + stop => $dtf->format_datetime($now),},]; + $retained->{malformed_profilemappings4} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t2),}, ] , []; + $retained->{correct_profile_mappings1} = [ { profile_id => $retained->{second_billing_profile_id}, + start => undef, + stop => undef, + network_id => undef }, + { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t2), + network_id => undef }, + { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t2), + stop => $dtf->format_datetime($t3), + network_id => undef } + ]; + $retained->{correct_profile_mappings2} = [ { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t2), + network_id=> undef }, + { profile_id => $billing_profile_id, + start => $dtf->format_datetime($t2), + stop => $dtf->format_datetime($t3), + network_id => undef }, + { profile_id => $retained->{second_billing_profile_id}, + start => $dtf->format_datetime($t3), + stop => undef, + network_id => $retained->{billing_network_id} } + ]; + } + +#multi-bill-prof POST: check 'start' timestamp is not in future +- + name: 'multi-bill-prof POST: check "start" timestamp is not in future' + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings1} + conditions: + is: + code: 422 + +#multi-bill-prof POST: check 'start' timestamp has to be before 'stop' timestamp +- + name: 'multi-bill-prof POST: check "start" timestamp has to be before "stop" timestamp' + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings2} + conditions: + is: + code: 422 + +#multi-bill-prof POST: check Interval with 'stop' timestamp but no 'start' timestamp specified +- + name: 'multi-bill-prof POST: check "Interval with "stop" timestamp but no "start" timestamp specified"' + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings3} + conditions: + is: + code: 422 + +#multi-bill-prof POST: check An initial interval without 'start' and 'stop' timestamps is required +- + name: 'multi-bill-prof POST: check An initial interval without "start" and "stop" timestamps is required' + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${malformed_profilemappings4} + conditions: + is: + code: 422 + +#multi-bill-prof: create test customer +- + name: 'multi-bill-prof: create test customer' + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${correct_profile_mappings1} + retain: + customer_path: header.location + conditions: + is: + code: 201 + +#get customer +- + name: GET customer + type: item + method: GET + path: '/${customer_path}' + retain: + customer: body + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{customer}->{all_billing_profiles}}; + $retained->{malformed_profilemappings4} = [ { profile_id => $retained->{billing_profile_id}, + start => undef, + stop => undef,}, ]; + } + conditions: + is: + code: 200 + '${customer}.billing_profile_id': ${second_billing_profile_id} + ok: + '${customer}.billing_profile_id': defined + '${customer}.billing_profiles': defined + '${customer}.all_billing_profiles': defined + is_deeply: + '${customer}.all_billing_profiles': ${correct_profile_mappings1} + +#multi-bill-prof PATCH: check 'start' timestamp is not in future +- + name: 'multi-bill-prof PATCH: check "start" timestamp is not in future' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings1} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: check 'start' timestamp has to be before 'stop' timestamp +- + name: 'multi-bill-prof PATCH: check "start" timestamp has to be before "stop" timestamp' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings2} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: check Interval with 'stop' timestamp but no 'start' timestamp specified +- + name: 'multi-bill-prof PATCH: check Interval with "stop" timestamp but no "start" timestamp specified' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings3} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: check Adding intervals without 'start' and 'stop' timestamps is not allowed. +- + name: 'multi-bill-prof PATCH: check Adding intervals without "start" and "stop" timestamps is not allowed.' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${malformed_profilemappings4} + conditions: + is: + code: 422 + +#multi-bill-prof PATCH: test customer with new billing profile +- + name: 'multi-bill-prof PATCH: test customer with new billing profile' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + retain: + patched_customer: body + content: + - + op: replace + path: /billing_profile_id + value: ${billing_profile_id} + perl_code: !!perl/code | + { + my ($retained) = @_; + + $retained->{posted_profiles_number} = scalar @{$retained->{correct_profile_mappings1}} + 1; + $retained->{actual_profiles_number} = scalar @{$retained->{patched_customer}->{all_billing_profiles}}; + + my $now = DateTime->now( + time_zone => DateTime::TimeZone->new(name => 'local') + ); + foreach my $m ( @{$retained->{patched_customer}->{billing_profiles}} ) { + if (!defined $m->{start}) { + push(@{$retained->{expected_mappings}},$m); + next; + } + my $s = $m->{start}; + $s =~ s/^(\d{4}\-\d{2}\-\d{2})\s+(\d.+)$/$1T$2/; + my $start = DateTime::Format::ISO8601->parse_datetime($s); + $start->set_time_zone( DateTime::TimeZone->new(name => 'local') ); + push(@{$retained->{expected_mappings}},$m) if ($start <= $now); + } + push ( @{$retained->{expected_mappings}}, @{$retained->{correct_profile_mappings2}} ); + } + conditions: + is: + code: 200 + '${patched_customer}.billing_profile_id': ${billing_profile_id} + '${posted_profiles_number}': '${actual_profiles_number}' + ok: + '${patched_customer}.billing_profile_id': defined + '${patched_customer}.billing_profiles': defined + '${patched_customer}.all_billing_profiles': defined + +#multi-bill-prof: patch test customer +- + name: 'multi-bill-prof: patch test customer' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + retain: + patched_customer: body + content: + - + op: replace + path: /billing_profiles + value: ${correct_profile_mappings2} + conditions: + is: + code: 200 + +#get customer +- + name: GET customer + type: item + method: GET + path: '/${customer_path}' + retain: + got_customer: body + conditions: + is: + code: 200 + '${patched_customer}.billing_profile_id': ${billing_profile_id} + ok: + '${patched_customer}.billing_profile_id': defined + '${patched_customer}.billing_profiles': defined + is_deeply: + '${got_customer}': '${patched_customer}' + +#multi-bill-prof: put test customer +- + name: 'multi-bill-prof: put test customer' + type: item + method: PUT + path: '/${customer_path}' + header: + Content-Type: application/json + Prefer: return=representation + retain: + updated_customer: body + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + max_subscriber: null + external_id: null + billing_profile_definition: profiles + billing_profiles: ${correct_profile_mappings2} + conditions: + is: + code: 200 + +#get customer +- + name: GET customer + type: item + method: GET + path: '/${customer_path}' + retain: + got_customer: body + conditions: + is: + code: 200 + '${updated_customer}.billing_profile_id': ${billing_profile_id} + ok: + '${updated_customer}.billing_profile_id': defined + '${updated_customer}.billing_profiles': defined + is_deeply: + '${got_customer}': '${updated_customer}' + + +#multi-bill-prof: terminate customer +- + name: 'multi-bill-prof: terminate customer' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + perl_code: !!perl/code | + { + my ($retained) = @_; + map { delete $_->{effective_start_time}; $_; } @{$retained->{patched_customer}->{billing_profiles}}; + map { delete $_->{effective_start_time}; $_; } @{$retained->{updated_customer}->{billing_profiles}}; + } + conditions: + is: + code: 200 + is_deeply: + #perform tests against stripped mapping here, because deleting start_time would have affected previous is_deeply verification + '${patched_customer}.billing_profiles': '${expected_mappings}' + '${updated_customer}.billing_profiles': '${expected_mappings}' + +#terminate billingprofile +- + name: 'terminate billingprofile' + type: item + method: PATCH + path: '/api/billingprofiles/${second_billing_profile_id}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 200 + +#terminate billingnetwork +- + name: 'terminate billingnetwork' + type: item + method: PATCH + path: '/api/billingnetworks/${billing_network_id}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 200 + +#prof-package: create another test billing profile +- + name: 'prof-package: create another test billing profile' + type: item + method: POST + path: '/api/billingprofiles/' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: THIRD test profile ${unique_id} + handle: third_testprofile${unique_id} + reseller_id: 1 + retain: + third_billing_profile_id: header.location + conditions: + is: + code: 201 + +#prof-package: create test billingnetwork +- + name: 'prof-package: create test billingnetwork' + type: item + method: POST + path: '/api/billingnetworks/' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: another test billing network ${unique_id} + description: another test billing network description ${unique_id} + reseller_id: 1 + blocks: + - + ip: '10.0.4.7' + mask: '26' + - + ip: '10.0.4.99' + mask: '26' + - + ip: '10.0.5.9' + mask: '24' + - + ip: '10.0.6.9' + mask: '24' + retain: + second_billing_network_id: header.location + conditions: + is: + code: 201 + perl_code: !!perl/code | + { + my ($retained) = @_; + $retained->{initial_profiles} = [ { profile_id => $retained->{billing_profile_id} }, + { + profile_id => $retained->{third_billing_profile_id}, + network_id => $retained->{second_billing_network_id} + } + ] + } + +#prof-package: create test profile package +- + name: 'prof-package: create test profile package' + type: item + method: POST + path: '/api/profilepackages/' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: test profile package ${unique_id} + description: test profile package description ${unique_id} + reseller_id: 1 + initial_profiles: ${initial_profiles} + retain: + profile_package_id: header.location + conditions: + is: + code: 201 + +#prof-package: create second test profile package +- + name: 'prof-package: create second test profile package' + type: item + method: POST + path: '/api/profilepackages/' + header: + Content-Type: application/json + Prefer: return=representation + content: + name: second test profile package ${unique_id} + description: second test profile package description ${unique_id} + reseller_id: 1 + initial_profiles: ${initial_profiles} + retain: + second_profile_package_id: header.location + conditions: + is: + code: 201 + +#prof-package: create test customer +- + name: 'prof-package: create test customer' + type: item + method: POST + path: '/api/customers/' + header: + Content-Type: application/json + content: + status: active + contact_id: ${customer_contact_id} + type: sipaccount + billing_profile_definition: package + max_subscribers: null + external_id: null + profile_package_id: ${profile_package_id} + retain: + customer_path: header.location + conditions: + is: + code: 201 + +#prof-package: get test customer +- + name: 'prof-package: get test customer' + type: item + method: GET + path: '${customer_path}' + header: + Content-Type: application/json + retain: + customer: body + conditions: + is: + code: 200 + '${customer}.billing_profile_id': ${third_billing_profile_id} + '${customer}.profile_package_id': ${profile_package_id} + ok: + '${customer}.billing_profile_id': defined + '${customer}.profile_package_id': defined + perl_code: !!perl/code | + { + my ($retained) = @_; + + my $dtf = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); + my $now = DateTime->now( + time_zone => DateTime::TimeZone->new(name => 'local') + ); + my $t1 = $now->clone->add(days => 1); + my $t2 = $now->clone->add(days => 2); + my $t3 = $now->clone->add(days => 3); + + $retained->{billing_profiles} = [ { profile_id => $retained->{billing_profile_id}, + start => $dtf->format_datetime($t1), + stop => $dtf->format_datetime($t2) } , + { profile_id => $retained->{third_billing_profile_id}, + network_id => $retained->{second_billing_network_id}, + start => $dtf->format_datetime($t2), + stop => $dtf->format_datetime($t3) } ]; + } + +#prof-package: patch test customer +- + name: 'prof-package: patch test customer' + type: item + method: PATCH + path: '${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profiles + value: ${billing_profiles} + retain: + customer: body + conditions: + is: + code: 200 + '${customer}.billing_profile_id': ${third_billing_profile_id} + '${customer}.profile_package_id': ${profile_package_id} + ok: + '${customer}.billing_profile_id': defined + '${customer}.profile_package_id': defined + +#prof-package: patch test customer +- + name: 'prof-package: patch test customer' + type: item + method: PATCH + path: '${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /billing_profile_id + value: ${billing_profile_id} + retain: + customer: body + conditions: + is: + code: 200 + '${customer}.billing_profile_id': ${billing_profile_id} + '${customer}.profile_package_id': ${profile_package_id} + ok: + '${customer}.billing_profile_id': defined + '${customer}.profile_package_id': defined + +#prof-package: patch test customer +- + name: 'prof-package: patch test customer' + type: item + method: PATCH + path: '${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /profile_package_id + value: ${profile_package_id} + retain: + customer: body + conditions: + is: + code: 200 + '${customer}.billing_profile_id': ${billing_profile_id} + '${customer}.profile_package_id': ${profile_package_id} + ok: + '${customer}.billing_profile_id': defined + '${customer}.profile_package_id': defined + +#prof-package: patch test customer +- + name: 'prof-package: patch test customer' + type: item + method: PATCH + path: '${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /profile_package_id + value: ${second_profile_package_id} + retain: + customer: body + conditions: + is: + code: 200 + '${customer}.billing_profile_id': ${third_billing_profile_id} + '${customer}.profile_package_id': ${second_profile_package_id} + ok: + '${customer}.billing_profile_id': defined + '${customer}.profile_package_id': defined + +#prof-package: terminate customer +- + name: 'prof-package: terminate customer' + type: item + method: PATCH + path: '/${customer_path}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 200 + +#prof-package: delete profile package +- + name: 'prof-package: delete profile package' + type: item + method: DELETE + path: '/api/profilepackages/${profile_package_id}' + conditions: + is: + code: 204 + +#prof-package: delete second profile package +- + name: 'prof-package: delete second profile package' + type: item + method: DELETE + path: '/api/profilepackages/${second_profile_package_id}' + conditions: + is: + code: 204 + +#prof-package: terminate third billing profile +- + name: 'prof-package: terminate third billing profile' + type: item + method: PATCH + path: '/api/billingprofiles/${third_billing_profile_id}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 200 + +#prof-package: terminate second billing network +- + name: 'prof-package: terminate second billing network' + type: item + method: PATCH + path: '/api/billingnetworks/${second_billing_network_id}' + header: + Content-Type: application/json-patch+json + Prefer: return=representation + content: + - + op: replace + path: /status + value: terminated + conditions: + is: + code: 200 + +#check locked status for deleting used contact +- + name: check locked status for deleting used contact + type: item + method: DELETE + path: '/${customer_contact_path}' + conditions: + is: + code: 423 diff --git a/t/lib/NGCP/TestFramework/RequestBuilder.pm b/t/lib/NGCP/TestFramework/RequestBuilder.pm new file mode 100644 index 0000000000..df4eaf3aef --- /dev/null +++ b/t/lib/NGCP/TestFramework/RequestBuilder.pm @@ -0,0 +1,105 @@ +package NGCP::TestFramework::RequestBuilder; + +use strict; +use warnings; +use HTTP::Request; +use Cpanel::JSON::XS; +use Moose; +use Data::Dumper; + +has 'base_uri' => ( + isa => 'Str', + is => 'ro' +); + +sub build { + my ( $self, $args ) = @_; + + my @methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE']; + + if ( !$args->{method} && !grep { $_ eq $args->{method} } @methods ) { + return { + success => 0, + message => 'HTTP method missing or incorrect!' + }; + } + + if ( !$args->{path} ) { + return { + success => 0, + message => 'Path missing!' + }; + } + + $self->_replace_vars($args); + + my $req = HTTP::Request->new( $args->{method}, $self->base_uri.$args->{path} ); + grep { $req->header( $_ => $args->{header}->{$_} ) } keys %{$args->{header}} if $args->{header}; + $req->content( encode_json( $args->{content} ) ) if $args->{content}; + + return $req; +} + +sub _replace_vars { + my ( $self, $args ) = @_; + + # substitute variables in path + if ( $args->{path} =~ /\$\{(.*)\}/ ) { + $args->{path} =~ s/\$\{(.*)\}/$args->{retain}->{$1}/; + } + + # substitute variables in content + if ( $args->{content} ) { + if ( ref $args->{content} eq 'HASH' ) { + foreach my $content_key (keys %{$args->{content}}) { + if ( $args->{content}->{$content_key} && $args->{content}->{$content_key} =~ /\$\{(.*)\}$/ ) { + if ( ref $args->{retain}->{$1} eq 'ARRAY' || ref $args->{retain}->{$1} eq 'HASH' ) { + $args->{content}->{$content_key} = $args->{retain}->{$1}; + } + else { + $args->{content}->{$content_key} =~ s/\$\{(.*)\}/$args->{retain}->{$1}/; + } + } + elsif ( $args->{content}->{$content_key} && $args->{content}->{$content_key} =~ /^\$\{(.*)\}\..+/ ) { + my @splitted_values = split (/\./, $args->{content}->{$content_key}); + $args->{content}->{$content_key} = $self->_retrieve_from_composed_key( \@splitted_values, $args->{retain} ); + } + } + } + elsif ( ref $args->{content} eq 'ARRAY' ) { + foreach my $content ( @{$args->{content}} ) { + foreach my $content_key (keys %$content) { + if ( $content->{$content_key} && $content->{$content_key} =~ /\$\{(.*)\}$/ ) { + if ( ref $args->{retain}->{$1} eq 'ARRAY' || ref $args->{retain}->{$1} eq 'HASH' ) { + $content->{$content_key} = $args->{retain}->{$1}; + } + else { + $content->{$content_key} =~ s/\$\{(.*)\}/$args->{retain}->{$1}/; + } + } + elsif ( $content->{$content_key} && $content->{$content_key} =~ /^\$\{(.*)\}\..+/ ) { + my @splitted_values = split (/\./, $content->{$content_key}); + $content->{$content_key} = $self->_retrieve_from_composed_key( \@splitted_values, $args->{retain} ); + } + } + } + } + else { + if ( $args->{content} =~ /\$\{(.*)\}/ ) { + $args->{content} = $args->{retain}->{$1}; + } + } + } +} + +sub _retrieve_from_composed_key { + my ( $self, $splitted_values, $retained ) = @_; + + if ( $splitted_values->[0] =~ /\$\{(.*)\}/ ) { + my $value = $retained->{$1}; + grep { $value = $value->{$splitted_values->[$_]} } (1..(scalar @$splitted_values - 1)); + return $value; + } +} + +1; diff --git a/t/lib/NGCP/TestFramework/TestExecutor.pm b/t/lib/NGCP/TestFramework/TestExecutor.pm new file mode 100644 index 0000000000..a0810b73ec --- /dev/null +++ b/t/lib/NGCP/TestFramework/TestExecutor.pm @@ -0,0 +1,115 @@ +package NGCP::TestFramework::TestExecutor; + +use strict; +use warnings; +use Cpanel::JSON::XS; +use Data::Walk; +use Moose; +use Test::More; +use Data::Dumper; + +sub run_tests { + my ( $self, $conditions, $result, $retained, $test_name ) = @_; + + foreach my $condition ( keys %$conditions ) { + if ( $condition eq 'is' ) { + while ( my ( $check_param, $check_value ) = each %{$conditions->{$condition}} ) { + if ( $check_value =~ /^\$\{(.*)\}$/ ) { + $check_value = $retained->{$1}; + } + if ( $check_param =~ /^\$\{(.*)\}$/ ) { + $check_param = $retained->{$1}; + } + if ( $check_param =~ /.+\..+/ ) { + my @splitted_values = split (/\./, $check_param); + $check_param = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained ); + is ($check_param, $check_value, $test_name); + } + elsif ( $check_param eq 'code' ) { + is ($result->code, $check_value, $test_name); + } + elsif ( $check_param eq 'header' ) { + foreach my $header_condition ( keys %{$conditions->{$condition}->{$check_param}} ) { + is ($result->header($header_condition), $check_value->{$header_condition}, $test_name); + } + } + else { + is ($check_param, $check_value, $test_name); + } + } + } + elsif ( $condition eq 'ok' ) { + foreach my $check_param (keys %{$conditions->{$condition}}) { + if ( $check_param eq 'options' ) { + my $body = decode_json($result->decoded_content); + my @hopts = split /\s*,\s*/, $result->header('Allow'); + ok(exists $body->{methods} && ref $body->{methods} eq "ARRAY", $test_name); + foreach my $opt(@{$conditions->{$condition}->{$check_param}}) { + ok(grep { /^$opt$/ } @hopts, $test_name); + ok(grep { /^$opt$/ } @{ $body->{methods} }, $test_name); + } + } + if ( $conditions->{$condition}->{$check_param} eq 'defined' || $conditions->{$condition}->{$check_param} eq 'undefined') { + if ( $check_param =~ /.+\..+/ ) { + my @splitted_values = split (/\./, $check_param); + my $check_value = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained ); + $conditions->{$condition}->{$check_param} eq 'defined' ? + ok(defined $check_value, $test_name) : ok(!defined $check_value, $test_name); + } + } + } + } + elsif ( $condition eq 'like' ) { + foreach my $check_param (keys %{$conditions->{$condition}}) { + if ( $check_param =~ /.+\..+/ ) { + my @splitted_values = split (/\./, $check_param); + my $check_value = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained ); + like ($check_value, qr/$conditions->{$condition}->{$check_param}/, $test_name); + } + } + } + elsif ( $condition eq 'is_deeply' ) { + foreach my $check_param (keys %{$conditions->{$condition}}) { + *replace_variables = sub { + if ( ref $_ eq 'HASH' ) { + while ( my ( $key, $value ) = each %$_ ) { + if ( $value && $value =~ /\$\{(.*)\}/ ) { + $_->{$key} = $retained->{$1}; + } + } + } + }; + + walkdepth {wanted => \&replace_variables}, $conditions->{$condition}->{$check_param}; + if ( $conditions->{$condition}->{$check_param} =~ /^\$\{(.*)\}$/ ) { + $conditions->{$condition}->{$check_param} = $retained->{$1}; + } + my $check_value; + if ( $check_param=~ /^\$\{(.*)\}$/ ) { + $check_value = $retained->{$1}; + } + if ( $check_param =~ /.+\..+/ ) { + my @splitted_values = split (/\./, $check_param); + $check_value = $self->_retrieve_from_composed_key( $result, \@splitted_values, $retained ); + } + is_deeply ($check_value, $conditions->{$condition}->{$check_param}, $test_name); + } + } + } +} + +sub _retrieve_from_composed_key { + my ( $self, $result, $splitted_values, $retained ) = @_; + + if ( $splitted_values->[0] eq 'body' ) { + my $body = decode_json($result->decoded_content); + return $body->{$splitted_values->[1]}; + } + elsif ( $splitted_values->[0] =~ /\$\{(.*)\}/ ) { + my $value = $retained->{$1}; + grep { $value = $value->{$splitted_values->[$_]} } (1..(scalar @$splitted_values - 1)); + return $value; + } +} + +1; diff --git a/t/lib/NGCP/testrunner.pl b/t/lib/NGCP/testrunner.pl new file mode 100644 index 0000000000..dda4626d3e --- /dev/null +++ b/t/lib/NGCP/testrunner.pl @@ -0,0 +1,81 @@ +use lib '..'; +use strict; +use warnings; +use NGCP::TestFramework; +use Test::More; +use threads; +use Thread::Queue; +use Data::Dumper; + +my $server = $ARGV[0] || undef; +my $selected = $ARGV[1] || 'all'; + +if ( !$server ){ + print "Usage: \$ perl testrunner.pl [<testsystem>] [<testset>]\n"; + print "Usage example: \$ perl testrunner.pl 192.168.88.162\n"; + print "Usage example: \$ perl testrunner.pl 192.168.88.162 fast\n"; + print "Possible test set: all, stable, fast, t/lib/NGCP/TestFramework/Interface/Contracts.yaml\n"; + print "Default test set: all\n"; + exit(1); +} + +my @test_files; + +if ( $selected eq 'stable' ) { + print "Test selection: stable\n"; +} +elsif ( $selected eq 'fast' ) { + print "Test selection: fast\n"; +} +elsif ( $selected eq 'all' ) { + print "Test selection: all\n"; + map { push @test_files, "TestFramework/Interface/$_" } `ls TestFramework/Interface`; +} +else{ + print "Test selection: $selected\n"; + push @test_files, $selected; +} + +print "################################################################################\n"; +print "Finished main setup, now running tests ...\n"; + +$ENV{CATALYST_SERVER_SUB}="https://$server:443"; +$ENV{CATALYST_SERVER}="https://$server:1443"; +$ENV{NGCP_SESSION_ID}=int(rand(1000)).time; + +my @threads; +my $tests_queue = Thread::Queue->new(); + +for ( @test_files ) { + $tests_queue->enqueue($_); +} + +$tests_queue->end(); + +for (1..2) { + push @threads, threads->create( {'context' => 'void'}, \&worker, $tests_queue ); +} + +foreach ( @threads ){ + $_->join(); +} + +done_testing(); + +sub worker { + my ($tests_queue) = @_; + + while ( my $test_file = $tests_queue->dequeue_nb() ) { + my $start_time = time; + print "Running tests from $test_file\n"; + my $test_framework = NGCP::TestFramework->new( {file_path => $test_file} ); + + my $result_code = $test_framework->run(); + + my $total_time = time - $start_time; + print "Finished test execution for $test_file, test execution returned with exit code $result_code.\n"; + print "Tests for $test_file took $total_time seconds.\n"; + } +} + +1;