File Coverage

lib/Test/Fake/HTTPD.pm
Criterion Covered Total %
statement 60 92 65.2
branch 5 36 13.8
condition 4 28 14.2
subroutine 21 28 75.0
pod 9 11 81.8
total 99 195 50.7


line stmt bran cond sub pod time code
1             package Test::Fake::HTTPD;
2              
3 3     3   489238 use 5.008_001;
  3         23  
4 3     3   21 use strict;
  3         8  
  3         63  
5 3     3   14 use warnings;
  3         6  
  3         82  
6 3     3   1748 use HTTP::Daemon;
  3         138524  
  3         32  
7 3     3   3225 use HTTP::Message::PSGI qw(res_from_psgi);
  3         19992  
  3         198  
8 3     3   563 use Test::TCP qw(wait_port);
  3         41323  
  3         151  
9 3     3   20 use URI;
  3         6  
  3         87  
10 3     3   18 use Time::HiRes ();
  3         6  
  3         54  
11 3     3   15 use Scalar::Util qw(blessed weaken);
  3         11  
  3         134  
12 3     3   19 use Carp qw(croak);
  3         5  
  3         132  
13 3     3   17 use Exporter qw(import);
  3         5  
  3         692  
14              
15             our $VERSION = '0.09';
16             $VERSION = eval $VERSION;
17              
18             our @EXPORT = qw(
19             run_http_server run_https_server
20             extra_daemon_args
21             );
22              
23             our $ENABLE_SSL = eval { require HTTP::Daemon::SSL; 1 };
24 1     1 0 85 sub enable_ssl { $ENABLE_SSL }
25              
26             our %EXTRA_DAEMON_ARGS = ();
27 0     0 0 0 sub extra_daemon_args (%) { %EXTRA_DAEMON_ARGS = @_ }
28              
29             sub run_http_server (&) {
30 1     1 1 1673 my $app = shift;
31 1         9 __PACKAGE__->new->run($app);
32             }
33              
34       0 1   sub run_https_server (&) {} # noop
35             if ($ENABLE_SSL) {
36 3     3   28 no warnings 'redefine';
  3         9  
  3         3084  
37             *run_https_server = sub (&) {
38             my $app = shift;
39             __PACKAGE__->new(scheme => 'https')->run($app);
40             };
41             }
42              
43             sub new {
44 1     1 1 4 my ($class, %args) = @_;
45 1         8 bless {
46             host => '127.0.0.1',
47             timeout => 5,
48             listen => 5,
49             scheme => 'http',
50             %args
51             }, $class;
52             }
53              
54             our $DAEMON_MAP = {
55             http => 'HTTP::Daemon',
56             https => 'HTTP::Daemon::SSL',
57             };
58              
59             sub _daemon_class {
60 0     0   0 my $self = shift;
61 0         0 return $DAEMON_MAP->{$self->{scheme}};
62             }
63              
64             sub run {
65 1     1 1 5 my ($self, $app) = @_;
66              
67             my %extra_daemon_args = $self->{daemon_args} && ref $self->{daemon_args} eq 'HASH'
68 1 50 33     12 ? %{ $self->{daemon_args} }
  0         0  
69             : %EXTRA_DAEMON_ARGS;
70              
71             $self->{server} = Test::TCP->new(
72             ($self->host ? (host => $self->host) : ()),
73             code => sub {
74 0     0   0 my $port = shift;
75              
76 0         0 my $d;
77 0         0 for (1..10) {
78             $d = $self->_daemon_class->new(
79             # Note: IO::Socket::IP ignores LocalAddr if LocalHost is set.
80             ($self->host ? (LocalAddr => $self->host) : ()),
81             LocalPort => $port,
82             Timeout => $self->{timeout},
83             Proto => 'tcp',
84             Listen => $self->{listen},
85 0 0       0 ($self->_is_win32 ? () : (ReuseAddr => 1)),
    0          
    0          
86             %extra_daemon_args,
87             ) and last;
88 0         0 Time::HiRes::sleep(0.1);
89             }
90              
91 0 0 0     0 croak(sprintf("failed to listen on address %s port %s%s",
    0 0        
92             $self->host || '',
93             $self->port || '',
94             $@ eq '' ? '' : ": $@")) unless $d;
95              
96 0         0 $d->accept; # wait for port check from parent process
97              
98 0         0 while (my $c = $d->accept) {
99 0         0 while (my $req = $c->get_request) {
100 0         0 my $res = $self->_to_http_res($app->($req));
101 0         0 $c->send_response($res);
102             }
103 0         0 $c->close;
104 0         0 undef $c;
105             }
106             },
107 1 50       5 ($self->{port} ? (port => $self->{port}) : ()),
    50          
108             );
109              
110 1         8804 weaken($self);
111 1         40 $self;
112             }
113              
114             sub scheme {
115 3     3 1 12 my $self = shift;
116 3         82 return $self->{scheme};
117             }
118              
119             sub host {
120 5     5 1 10 my $self = shift;
121 5         25 return $self->{host};
122             }
123              
124             sub port {
125 4     4 1 3403 my $self = shift;
126 4 50       34 return $self->{server} ? $self->{server}->port : 0;
127             }
128              
129             sub host_port {
130 1     1 1 737 my $self = shift;
131 1         18 return $self->endpoint->host_port;
132             }
133              
134             sub endpoint {
135 3     3 1 9734 my $self = shift;
136 3         18 my $uri = URI->new($self->scheme . ':');
137 3         9840 my $host = $self->host;
138 3 50 33     42 $host = 'localhost' if !defined($host) || $host eq '' || $host eq '0.0.0.0' || $host eq '::';
      33        
      33        
139 3         27 $uri->host($host);
140 3         350 $uri->port($self->port);
141 3         245 return $uri;
142             }
143              
144 0     0     sub _is_win32 { $^O eq 'MSWin32' }
145              
146             sub _is_psgi_res {
147 0     0     my ($self, $res) = @_;
148 0 0         return unless ref $res eq 'ARRAY';
149 0 0         return unless @$res == 3;
150 0 0 0       return unless $res->[0] && $res->[0] =~ /^\d{3}$/;
151 0 0 0       return unless ref $res->[1] eq 'ARRAY' || ref $res->[1] eq 'HASH';
152 0           return 1;
153             }
154              
155             sub _to_http_res {
156 0     0     my ($self, $res) = @_;
157              
158 0           my $http_res;
159 0 0 0       if (blessed($res) and $res->isa('HTTP::Response')) {
    0 0        
    0          
160 0           $http_res = $res;
161             }
162             elsif (blessed($res) and $res->isa('Plack::Response')) {
163 0           $http_res = res_from_psgi($res->finalize);
164             }
165             elsif ($self->_is_psgi_res($res)) {
166 0           $http_res = res_from_psgi($res);
167             }
168              
169 0 0         croak(sprintf '%s: response must be HTTP::Response or Plack::Response or PSGI', __PACKAGE__)
170             unless $http_res;
171              
172 0           return $http_res;
173             }
174              
175             1;
176              
177             =head1 NAME
178              
179             Test::Fake::HTTPD - a fake HTTP server
180              
181             =head1 SYNOPSIS
182              
183             DSL-style
184              
185             use Test::Fake::HTTPD;
186              
187             my $httpd = run_http_server {
188             my $req = shift;
189             # ...
190              
191             # 1. HTTP::Response ok
192             return $http_response;
193             # 2. Plack::Response ok
194             return $plack_response;
195             # 3. PSGI response ok
196             return [ 200, [ 'Content-Type' => 'text/plain' ], [ 'Hello World' ] ];
197             };
198              
199             printf "Listening on address:port %s\n", $httpd->host_port;
200             # or
201             printf "Listening on address %s port %s\n", $httpd->host, $httpd->port;
202              
203             # access to fake HTTP server
204             use LWP::UserAgent;
205             my $res = LWP::UserAgent->new->get($httpd->endpoint); # "http://127.0.0.1:{port}"
206              
207             # Stop http server automatically at destruction time.
208              
209             OO-style
210              
211             use Test::Fake::HTTPD;
212              
213             my $httpd = Test::Fake::HTTPD->new(
214             timeout => 5,
215             daemon_args => { ... }, # HTTP::Daemon args
216             );
217              
218             $httpd->run(sub {
219             my $req = shift;
220             # ...
221             [ 200, [ 'Content-Type', 'text/plain' ], [ 'Hello World' ] ];
222             });
223              
224             # Stop http server automatically at destruction time.
225              
226             =head1 DESCRIPTION
227              
228             Test::Fake::HTTPD is a fake HTTP server module for testing.
229              
230             =head1 FUNCTIONS
231              
232             =over 4
233              
234             =item * C
235              
236             Starts HTTP server and returns the guard instance.
237              
238             my $httpd = run_http_server {
239             my $req = shift;
240             # ...
241             return $http_or_plack_or_psgi_res;
242             };
243              
244             # can use $httpd guard object, same as OO-style
245             LWP::UserAgent->new->get($httpd->endpoint);
246              
247             =item * C
248              
249             Starts B server and returns the guard instance.
250              
251             If you use this method, you MUST install L.
252              
253             extra_daemon_args
254             SSL_key_file => "certs/server-key.pem",
255             SSL_cert_file => "certs/server-cert.pem";
256              
257             my $httpd = run_https_server {
258             my $req = shift;
259             # ...
260             return $http_or_plack_or_psgi_res;
261             };
262              
263             # can use $httpd guard object, same as OO-style
264             my $ua = LWP::UserAgent->new(
265             ssl_opts => {
266             SSL_verify_mode => 0,
267             verify_hostname => 0,
268             },
269             );
270             $ua->get($httpd->endpoint);
271              
272             =back
273              
274             =head1 METHODS
275              
276             =over 4
277              
278             =item * C
279              
280             Returns a new instance.
281              
282             my $httpd = Test::Fake::HTTPD->new(%args);
283              
284             C<%args> are:
285              
286             =over 8
287              
288             =item * C
289              
290             timeout value (default: 5)
291              
292             =item * C
293              
294             queue size for listen (default: 5)
295              
296             =item * C
297              
298             local address to listen on (default: 127.0.0.1)
299              
300             =item * C
301              
302             TCP port to listen on (default: auto detection)
303              
304             =back
305              
306             my $httpd = Test::Fake::HTTPD->new(
307             timeout => 10,
308             listen => 10,
309             port => 3333,
310             );
311              
312             =item * C
313              
314             Starts this HTTP server.
315              
316             $httpd->run(sub { ... });
317              
318             =item * C
319              
320             Returns a scheme of running, "http" or "https".
321              
322             my $scheme = $httpd->scheme;
323              
324             =item * C
325              
326             Returns the address the server is listening on.
327              
328             =item * C
329              
330             Returns the TCP port the server is listening on.
331              
332             my $port = $httpd->port;
333              
334             =item * C
335              
336             Returns the host:port from C (e.g., "127.0.0.1:1234", "[::1]:1234").
337              
338             my $host_port = $httpd->host_port;
339              
340             =item * C
341              
342             Returns a URI object to the running server (e.g., "http://127.0.0.1:1234",
343             "https://[::1]:1234"). If C returns C, C<''>, C<'0.0.0.0'>,
344             or C<'::'>, the host portion of the URI is set to C.
345              
346             use LWP::UserAgent;
347              
348             my $res = LWP::UserAgent->new->get($httpd->endpoint);
349              
350             my $url = $httpd->endpoint;
351             $url->path('/foo/bar');
352             my $res = LWP::UserAgent->new->get($url);
353              
354             =back
355              
356             =head1 AUTHOR
357              
358             NAKAGAWA Masaki Emasaki@cpan.orgE
359              
360             =head1 THANKS TO
361              
362             xaicron
363              
364             =head1 LICENSE
365              
366             This library is free software; you can redistribute it and/or modify
367             it under the same terms as Perl itself.
368              
369             =head1 SEE ALSO
370              
371             L, L, L, L
372              
373             =cut