From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#! /usr/bin/perl
#
# Copyright (c) 2019 cPanel, L.L.C.
# All rights reserved.
#
# Distributed under the terms of the MIT license. See the LICENSE file for
# further details.
#
use strict;
use JSON ();
use lib 't/lib';
use Test::More qw(no_plan);
{
throws_ok {
OpenStack::Client->new
} qr/No API endpoint provided/, "OpenStack::Client->new() dies when no API endpoint URI is provided";
lives_ok {
OpenStack::Client->new('http://foo.bar/')
} "OpenStack::Client->new() doesn't die when API endpoint is provided";
}
{
my $endpoint = 'http://foo.bar/';
my $token = {
'foo' => 'bar'
};
my %tests = (
'foobar' => "${endpoint}foobar",
'/foobar' => "${endpoint}foobar",
'foobar/baz' => "${endpoint}foobar/baz"
);
my $client = OpenStack::Client->new($endpoint,
'token' => $token
);
foreach my $input (sort keys %tests) {
my $expected = $tests{$input};
is($client->uri($input) => $expected, "\$client->uri() yields '$expected' for '$input'");
}
is($client->endpoint => $endpoint, "\$client->endpoint() returns original HTTP endpoint URI");
is($client->token => $token, "\$client->token() returns original token object");
}
Test::OpenStack::Client->run_client_tests({
'responses' => [{
'content' => JSON::encode_json({
'foo' => 'bar'
})
}],
'test' => sub {
my ($client) = @_;
my $content;
lives_ok {
$content = $client->call('GET' => '/foo');
} "\$client->call() doesn't die on a well-instantiated client";
is($content->{'foo'} => 'bar', "Response contains expected result");
}
}, {
'test' => sub {
is(shift->call('GET' => '/foo') => '', "Empty response contains expected empty result");
}
}, {
'responses' => [{
'code' => 420,
'content' => '420 Baked',
'headers' => {
'content-type' => 'text/plain'
}
}],
'test' => sub {
my ($client) = @_;
throws_ok {
$client->call('GET' => '/foo')
} qr/420 Baked/, "\$client->call() dies when 400 error is returned by service";
}
}, {
'test' => sub {
my ($client, $ua) = @_;
my $content = {
'bar' => 'baz'
};
$client->call('GET' => '/foo', $content);
my $got = $ua->{'requests'}->[0]->content;
my $expected = JSON::encode_json($content);
is_deeply($got => $expected, "\$client decodes JSON response bodies");
}
}, {
'responses' => [{
'code' => 520,
'content' => 'Temporarily out of fucks to give',
'headers' => {
'Content-Type' => 'text/plain'
}
}],
'test' => sub {
my ($client) = @_;
throws_ok {
$client->call('GET' => '/foo');
} qr/Temporarily out of fucks to give/, "\$client->call() dies with response body when 400 or 500 status is raised";
}
}, {
'responses' => [{
'code' => 320,
'content' => 'Do whatever you want',
'headers' => {
'Content-Type' => 'text/plain'
}
}],
'test' => sub {
my ($client) = @_;
my $got = $client->call('GET' => '/foo');
my $expected = 'Do whatever you want';
is($got => $expected, "\$client->call() returns message body for plain text, non-error responses");
}
}, {
'responses' => [{
'code' => 504,
'content' => undef
}],
'test' => sub {
my ($client) = @_;
throws_ok {
$client->call('GET' => '/foo');
} qr/504 Unknown error/, "\$client->call() dies with an unknown error when no response body is provided";
}
}, {
'test' => sub {
my ($client, $ua) = @_;
my @tests = (
['/foo' => { }, "no parameters"],
['/foo?bar=baz' => {
'bar' => 'baz'
}, "a single parameter"],
['/foo?bar=baz&meow=cats' => {
'bar' => 'baz',
'meow' => 'cats'
}, "multiple parameters"],
['/foo?bar=baz&tiny%20kittens=are%20awesome' => {
'bar' => 'baz',
'tiny kittens' => 'are awesome'
}, "parameters needing URI encoding"]
);
my $i = 0;
my $prefix = 'http://foo.bar';
foreach my $test (@tests) {
my ($expected_suffix, $input, $message) = @{$test};
my $expected = "${prefix}${expected_suffix}";
$client->get('/foo', %{$input});
my $got = $ua->{'requests'}->[$i++]->{'path'};
is($got => $expected, "\$client->get() encodes with $message");
}
}
}, {
'test' => sub {
my ($client, $ua) = @_;
$client->get('/foo?bar=baz',
'meow' => 'cats'
);
my $prefix = 'http://foo.bar';
my $got = $ua->{'requests'}->[0]->{'path'};
my $expected = "${prefix}/foo?bar=baz&meow=cats";
is($got => $expected, "\$client->get() handles request paths with arguments already present");
}
}, {
'responses' => [{
'content' => JSON::encode_json({
'payload' => ['foo', 'bar', 'baz']
})
}],
'test' => sub {
my ($client) = @_;
my $got = [];
my $expected = ['foo', 'bar', 'baz'];
$client->each('/foo', sub {
my ($result) = @_;
push @{$got}, @{$result->{'payload'}};
});
is_deeply($got => $expected, "\$client->each() calls a callback for each paginated result");
throws_ok {
$client->each();
} qr/Invalid number of arguments/, "\$client->each() dies when too few arguments are passed";
throws_ok {
$client->each('/foo', {}, sub {}, 'foo');
} qr/Invalid number of arguments/, "\$client->each() dies when too many arguments are passed";
}
}, {
'responses' => [{
'content' => JSON::encode_json({})
}],
'test' => sub {
my ($client, $ua) = @_;
lives_ok {
$client->each('/foo', {
'bar' => 'baz'
}, sub {});
} "\$client->each() succeeds when passed GET parameters";
my $got = $ua->{'requests'}->[0]->{'path'};
my $expected = "http://foo.bar/foo?bar=baz";
is($got => $expected, "\$client->each() handles GET parameters properly");
}
}, {
'responses' => [{
'content' => JSON::encode_json({
'items' => [{
'foo' => 'bar'
}],
'next' => '/foo/1'
})
}, {
'content' => JSON::encode_json({
'items' => [{
'baz' => 'boo'
}],
'next' => '/foo/2'
})
}, {
'content' => JSON::encode_json({
'items' => [{
'meow' => 'cats'
}]
})
}],
'test' => sub {
my ($client) = @_;
my @responses;
$client->every('/foo', 'items', sub {
my ($item) = @_;
push @responses, $item;
});
is(scalar @responses => 3, "\$client->every() callback is called once per item across paginations");
my @expected = (
{'foo' => 'bar'},
{'baz' => 'boo'},
{'meow' => 'cats'}
);
is_deeply(\@responses => \@expected, "\$client->every() decodes response bodies as expected");
}
}, {
'responses' => [{
'content' => JSON::encode_json({
'items' => ['foo', 'bar', 'baz']
})
}],
'test' => sub {
my ($client, $ua) = @_;
lives_ok {
$client->every('/foo', 'items', {
'meow' => 'cats'
}, sub {});
} "\$client->every() won't die if passed GET parameters";
my $got = $ua->{'requests'}->[0]->{'path'};
my $expected = "http://foo.bar/foo?meow=cats";
is($got => $expected, "\$client->every() passes along GET parameters fine");
}
}, {
'responses' => [{
'content' => '{}'
}],
'test' => sub {
my ($client) = @_;
throws_ok {
$client->every('/foo', 'items', sub {})
} qr/Response from \/foo does not contain attribute/, "\$client->every() dies when responses lack attribute corresponding to result set";
throws_ok {
$client->every('/foo', 'items', sub {}, {}, 'bleh');
} qr/Invalid number of arguments/, "\$client->every() will complain if argument count is too much";
throws_ok {
$client->every('/foo', 'items');
} qr/Invalid number of arguments/, "\$client->every() will complain if argument count is too little";
}
}, {
'responses' => [{
'content' => JSON::encode_json({
'items' => ['foo', 'bar', 'baz'],
'next' => '/foo/2'
})
}, {
'content' => JSON::encode_json({
'items' => ['eins', 'zwei', 'drei']
})
}],
'test' => sub {
my ($client) = @_;
my @got;
my @expected = qw(foo bar baz eins zwei drei);
lives_ok {
@got = $client->all('/foo', 'items');
} "\$client->all() doesn't die in ordinary circumstances";
is_deeply(\@got => \@expected, "\$client->all() handles result sets in the appropriate manner");
}
}, {
'responses' => [{
'content' => JSON::encode_json({})
}],
'test' => sub {
my ($client, $ua) = @_;
my $sub = sub {};
lives_ok {
$client->call('PUT', {}, '/foo', $sub);
} "\$client->call() doesn't die when passed a CODE ref request body";
is $ua->{'requests'}->[0]->{'content'} => $sub, "\$client->call() passes CODE ref request body appropriately";
}
}, {
'responses' => [{
'content' => JSON::encode_json({})
}],
'test' => sub {
my ($client, $ua) = @_;
my $called = 0;
lives_ok {
$client->call({
'method' => 'PUT',
'path' => '/foo',
'handler' => sub {
$called = 1;
}
});
} "\$client->call() doesn't die when passed a CODE ref response handler";
}
}, {
'responses' => [{
'content' => JSON::encode_json({
'items' => []
})
}],
'test' => sub {
my ($client, $ua) = @_;
lives_ok {
$client->all('/foo', 'items', {
'meow' => 'cats'
});
} "\$client->all() doesn't die when provided GET parameters";
my $got = $ua->{'requests'}->[0]->{'path'};
my $expected = "http://foo.bar/foo?meow=cats";
is($got => $expected, "\$client->all() passes GET parameters on expectedly");
}
}, {
'responses' => [{
'content' => JSON::encode_json({
'bar' => 'baz'
})
}, {
'content' => JSON::encode_json({
'meow' => 'cats'
})
}],
'test' => sub {
my ($client, $ua) = @_;
my @tests = (
['put', 'PUT' => {'bar' => 'baz'}],
['post', 'POST' => {'meow' => 'cats'}]
);
my @got_responses;
lives_ok {
push @got_responses, $client->put('/foo', {'bar' => 'baz'});
} "\$client->put() doesn't die when called normally";
lives_ok {
push @got_responses, $client->post('/foo', {'meow' => 'cats'});
} "\$client->post() doesn't die when called normally";
lives_ok {
$client->delete('/foo/1');
} "\$client->delete() doesn't die when called normally";
my $i = 0;
foreach my $test (@tests) {
my ($name, $expected_method, $expected_response) = @{$test};
my $got_method = $ua->{'requests'}->[$i]->method;
my $got_response = $got_responses[$i];
is( $got_method => $expected_method, "\$client->$name() issues call with appropriate HTTP method");
is_deeply($got_response => $expected_response, "\$client->$name() decodes response body appropriately");
$i++;
}
}
}, {
'test' => sub {
my ($client, $ua) = @_;
no warnings qw/redefine/;
my $orig_request = \&Test::OpenStack::Client::UserAgent::request;
my $req_headers;
local *Test::OpenStack::Client::UserAgent::request = sub {
my ($self, $request) = @_;
# save headers for easy comparing
$req_headers = $request->{'headers'};
return $orig_request->($self, $request);
};
my @got_responses = ();
my $headers = {
'content-type' => 'foobar', 'x-grumpy-cat' => '0 ftg',
};
my $response;
lives_ok {
$response = $client->call('PATCH', $headers, '/foo', {'bar' => 'baz'});
} "\$client->call() doesn't die when called normally with 4 arguments (including \$headers)";
# expected/defaults for comparing
$headers->{'accept-encoding'} = 'identity, gzip, deflate, compress';
$headers->{'content-length'} = 0;
$headers->{'accept'} = 'application/json, text/plain';
is_deeply $req_headers => $headers, "4 argument form of \$client->call() sets headers as expected.";
delete $headers->{'content-length'};
delete $headers->{'x-grumpy-cat'};
# change defaults for comparing
$headers->{'accept-encoding'} = 'I have no idea what I am doing.';
$headers->{'accept'} = 'The honeybadger accepts it all!';
$headers->{'content-type'} = 'application/openstack-images-v2.1-json-patch';
$headers->{'x-ftg'} = 0;
$req_headers = {};
lives_ok {
$response = $client->call('PATCH', $headers, '/foo', {'bar' => 'baz'});
} "\$client->call() doesn't die when called normally with 4 arguments (including \$headers)";
# add this expected header for comparing
$headers->{'content-length'} = 0;
is_deeply $req_headers => $headers, "4 argument form of \$client->call() sets headers as expected.";
}
});