File Coverage

blib/lib/Test/RestAPI.pm
Criterion Covered Total %
statement 73 79 92.4
branch 8 12 66.6
condition n/a
subroutine 19 20 95.0
pod 3 3 100.0
total 103 114 90.3


line stmt bran cond sub pod time code
1             package Test::RestAPI;
2 9     9   8342 use Moo;
  9         79404  
  9         43  
3              
4             our $VERSION = '0.1.5';
5              
6 9     9   14095 use Types::Standard qw(ArrayRef InstanceOf Int Str);
  9         423294  
  9         78  
7 9     9   9872 use Test::RestAPI::Endpoint qw(convert_path_to_filename);
  9         27  
  9         545  
8 9     9   3215 use Test::RestAPI::MojoGenerator;
  9         21  
  9         256  
9 9     9   3495 use Port::Selector;
  9         34121  
  9         194  
10 9     9   40 use Path::Tiny;
  9         57  
  9         373  
11 9     9   422 use Mojo::JSON qw(decode_json);
  9         1979  
  9         340  
12 9     9   471 use Mojo::UserAgent;
  9         96389  
  9         113  
13              
14 9     9   229 use constant WINDOWS => ($^O eq 'MSWin32');
  9         11  
  9         649  
15              
16             BEGIN {
17 9     9   5880 if (WINDOWS) {
18             ## no critic (ProhibitStringyEval)
19             eval q{
20             use Win32::Process qw(NORMAL_PRIORITY_CLASS);
21             };
22              
23             die $@ if $@;
24             }
25             }
26              
27             =head1 NAME
28              
29             Test::RestAPI - Real mock of REST API
30              
31             =head1 SYNOPSIS
32              
33             my $api = Test::RestAPI->new(
34             endpoints => [
35             Test::RestAPI::Endpoint->new(
36             endpoint => '/a',
37             method => 'any',
38             )
39             ],
40             );
41              
42             $api->start();
43              
44             HTTP::Tiny->new->get($api->uri.'/test');
45              
46              
47             =head1 DESCRIPTION
48              
49             In many (test) case you need mock some REST API. One way is mock your REST-API class abstraction or HTTP client.
50             This module provides other way - start generated L server and provides pseudo-real your defined API.
51              
52             =head1 METHODS
53              
54             =head2 new(%attribute)
55              
56             =head3 %attribute
57              
58             =head4 endpoints
59              
60             I of instances L
61              
62             default is I (root) 200 OK - hello:
63              
64             Test::RestAPI::Endpoint->new(
65             endpoint => '/',
66             method => 'any',
67             body => 'Hello',
68             );
69              
70             =cut
71             has 'endpoints' => (
72             is => 'ro',
73             isa => ArrayRef [ InstanceOf ['Test::RestAPI::Endpoint'] ],
74             default => sub {
75             return [
76             Test::RestAPI::Endpoint->new(
77             path => '/',
78             method => 'any',
79             render => {text => 'Hello'},
80             )
81             ];
82             }
83             );
84              
85             =head4 mojo_app_generator
86              
87             This attribute is used for generating mojo application.
88              
89             default is L
90              
91             =cut
92             has 'mojo_app_generator' => (
93             is => 'ro',
94             isa => InstanceOf ['Test::RestAPI::MojoGenerator'],
95             default => sub {
96             return Test::RestAPI::MojoGenerator->new();
97             }
98             );
99              
100             has 'pid' => (
101             is => 'rw',
102             isa => Int,
103             );
104              
105             has 'uri' => (
106             is => 'rw',
107             isa => Str,
108             );
109              
110             has 'mojo_home' => (
111             is => 'ro',
112             default => sub {
113             my $mojo_home = Path::Tiny->tempdir();
114              
115             path($mojo_home, 'log')->mkpath();
116              
117             return $mojo_home;
118             }
119             );
120              
121             =head3 start
122              
123             Start REST API (L) application on some random unused port
124             and wait to initialize.
125              
126             For start new process is used C on non-windows machines and L for windows machines.
127              
128             For generating L application is used L in C attribute - is possible set own generator.
129              
130             =cut
131             sub start {
132 8     8 1 362 my ($self) = @_;
133              
134 8         90 my $app_path = $self->mojo_app_generator->create_app($self->endpoints);
135              
136 8         34 $self->pid($self->_start($app_path));
137             }
138              
139             sub _start {
140 8     8   16 my ($self, $app_path) = @_;
141              
142 8         22 $self->_create_uri();
143              
144 8         4348 my $pid;
145 8         8 if (WINDOWS) {
146             $pid = $self->_start_win($app_path);
147             }
148             else {
149 8         34 $pid = $self->_start_fork($app_path);
150             }
151              
152 4         79 $self->_wait_to_start();
153              
154 4         22 return $pid;
155             }
156              
157             sub _create_uri {
158 8     8   12 my ($self) = @_;
159              
160 8         102 my $port = Port::Selector->new->port();
161              
162 8         5530 $self->uri("http://localhost:$port");
163             }
164              
165             sub _start_win {
166 0     0   0 my ($self, $app_path) = @_;
167              
168             #This trick is copied from IPC::System::Simple
169             #If is check in this sub to non-Win32 system,
170             #perl don't check NORMAL_PRIORITY_CLASS constant in compilation phase.
171 0         0 if (!WINDOWS) {
172 0         0 die '_start_win ca be called only anna Windows';
173             }
174             else {
175             my $args = 'perl '.$app_path->canonpath().' '.join ' ', $self->_mojo_args();
176              
177             Win32::Process::Create(
178             my $proc,
179             $^X,
180             $args,
181             0,
182             NORMAL_PRIORITY_CLASS,
183             "."
184             ) || die "Process $args start fail $^E";
185              
186             return $proc->GetProcessID();
187             }
188             }
189              
190             sub _start_fork {
191 8     8   14 my ($self, $app_path) = @_;
192              
193 8         38 my @args = ($^X, $app_path->stringify, $self->_mojo_args());
194              
195 8         8662 my $pid = fork;
196              
197 8 100       460 if ($pid) {
    50          
198 4         224 return $pid
199             }
200             elsif ($pid == 0) {
201 4         46 exec {$args[0]} @args;
  4         0  
202 0         0 exit 1;
203             }
204             else {
205 0         0 die "Fork problem: $!";
206             }
207             }
208              
209             sub _mojo_args {
210 8     8   36 my ($self) = @_;
211              
212 8         124 return ('daemon', '-l', $self->uri, '-m', 'production', '--home', $self->mojo_home->canonpath());
213             }
214              
215             sub _wait_to_start {
216 4     4   22 my ($self) = @_;
217              
218 4         24 while (1) {
219 8 100       4000992 if (Mojo::UserAgent->new->get($self->uri.'/app_mojo_healtcheck')->res->body() eq 'OK') {
220 4         34909 return 1;
221             }
222 4         18783 sleep 1;
223             }
224             }
225              
226             =head2 count_of_requests($path)
227              
228             return count of request to C<$path> endpoint
229              
230             =cut
231             sub count_of_requests {
232 2     2 1 12173 my ($self, $path) = @_;
233              
234 2 50       11 $path = '/' if !defined $path;
235              
236 2         22 my $fh = path($self->mojo_home, convert_path_to_filename($path))->filehandle();
237              
238 2         266 my $lines = 0;
239 2         31 while (<$fh>) {
240 3         11 $lines++;
241             }
242              
243 2         34 return $lines;
244             }
245              
246             =head2 list_of_requests_body($path)
247              
248             return list (ArrayRef) of requests body to C<$path> endpoint
249              
250             =cut
251             sub list_of_requests_body {
252 1     1 1 11027 my ($self, $path) = @_;
253              
254 1 50       8 $path = '/' if !defined $path;
255              
256 1         13 my $fh = path($self->mojo_home, convert_path_to_filename($path))->filehandle();
257              
258 1         141 my @lines;
259 1         21 while (my $line = <$fh>) {
260 2         73 chomp $line;
261              
262 2         17 push @lines, decode_json($line);
263             }
264              
265 1         53 return \@lines;
266             }
267              
268              
269             sub DESTROY {
270 4     4   24862 my ($self) = @_;
271              
272 4 50       28 if ($^O eq 'MSWin32') {
273 0         0 Win32::Process::KillProcess($self->pid, 0);
274             }
275             else {
276 4         155 kill 'SIGTERM', $self->pid;
277             }
278             }
279              
280             =head1 LICENSE
281              
282             Copyright (C) Avast Software.
283              
284             This library is free software; you can redistribute it and/or modify
285             it under the same terms as Perl itself.
286              
287             =head1 AUTHOR
288              
289             Jan Seidl Eseidl@avast.comE
290              
291             =cut
292              
293             1;