1package Test::Nginx::LWP;
2
3use lib 'lib';
4use lib 'inc';
5use Test::Base -Base;
6
7our $VERSION = '0.17';
8
9our $NoLongString;
10
11use LWP::UserAgent;
12use Time::HiRes qw(sleep);
13use Test::LongString;
14use Test::Nginx::Util qw(
15    setup_server_root
16    write_config_file
17    get_canon_version
18    get_nginx_version
19    trim
20    show_all_chars
21    parse_headers
22    run_tests
23    $ServerPortForClient
24    $PidFile
25    $ServRoot
26    $ConfFile
27    $ServerPort
28    $RunTestHelper
29    $NoNginxManager
30    $RepeatEach
31    worker_connections
32    master_process_enabled
33    config_preamble
34    repeat_each
35    no_shuffle
36    no_root_location
37);
38
39our $UserAgent = LWP::UserAgent->new;
40$UserAgent->agent(__PACKAGE__);
41#$UserAgent->default_headers(HTTP::Headers->new);
42
43#use Smart::Comments::JSON '##';
44
45our @EXPORT = qw( plan run_tests run_test
46    repeat_each config_preamble worker_connections
47    master_process_enabled
48    no_long_string no_shuffle no_root_location);
49
50sub no_long_string () {
51    $NoLongString = 1;
52}
53
54sub run_test_helper ($$);
55
56$RunTestHelper = \&run_test_helper;
57
58sub parse_request ($$) {
59    my ($name, $rrequest) = @_;
60    open my $in, '<', $rrequest;
61    my $first = <$in>;
62    if (!$first) {
63        Test::More::BAIL_OUT("$name - Request line should be non-empty");
64        die;
65    }
66    $first =~ s/^\s+|\s+$//g;
67    my ($meth, $rel_url) = split /\s+/, $first, 2;
68    my $url = "http://localhost:$ServerPortForClient" . $rel_url;
69
70    my $content = do { local $/; <$in> };
71    if ($content) {
72        $content =~ s/^\s+|\s+$//s;
73    }
74
75    close $in;
76
77    return {
78        method  => $meth,
79        url     => $url,
80        content => $content,
81    };
82}
83
84sub chunk_it ($$$) {
85    my ($chunks, $start_delay, $middle_delay) = @_;
86    my $i = 0;
87    return sub {
88        if ($i == 0) {
89            if ($start_delay) {
90                sleep($start_delay);
91            }
92        } elsif ($middle_delay) {
93            sleep($middle_delay);
94        }
95        return $chunks->[$i++];
96    }
97}
98
99sub run_test_helper ($$) {
100    my ($block, $dry_run) = @_;
101
102    my $request = $block->request;
103
104    my $name = $block->name;
105    #if (defined $TODO) {
106    #$name .= "# $TODO";
107    #}
108
109    my $req_spec = parse_request($name, \$request);
110    ## $req_spec
111    my $method = $req_spec->{method};
112    my $req = HTTP::Request->new($method);
113    my $content = $req_spec->{content};
114
115    if (defined ($block->request_headers)) {
116        my $headers = parse_headers($block->request_headers);
117        while (my ($key, $val) = each %$headers) {
118            $req->header($key => $val);
119        }
120    }
121
122    #$req->header('Accept', '*/*');
123    $req->url($req_spec->{url});
124    if ($content) {
125        if ($method eq 'GET' or $method eq 'HEAD') {
126            croak "HTTP 1.0/1.1 $method request should not have content: $content";
127        }
128        $req->content($content);
129    } elsif ($method eq 'POST' or $method eq 'PUT') {
130        my $chunks = $block->chunked_body;
131        if (defined $chunks) {
132            if (!ref $chunks or ref $chunks ne 'ARRAY') {
133
134                Test::More::BAIL_OUT("$name - --- chunked_body should takes a Perl array ref as its value");
135            }
136
137            my $start_delay = $block->start_chunk_delay || 0;
138            my $middle_delay = $block->middle_chunk_delay || 0;
139            $req->content(chunk_it($chunks, $start_delay, $middle_delay));
140            if (!defined $req->header('Content-Type')) {
141                $req->header('Content-Type' => 'text/plain');
142            }
143        } else {
144            if (!defined $req->header('Content-Type')) {
145                $req->header('Content-Type' => 'text/plain');
146            }
147
148            $req->header('Content-Length' => 0);
149        }
150    }
151
152    if ($block->more_headers) {
153        my @headers = split /\n+/, $block->more_headers;
154        for my $header (@headers) {
155            next if $header =~ /^\s*\#/;
156            my ($key, $val) = split /:\s*/, $header, 2;
157            #warn "[$key, $val]\n";
158            $req->header($key => $val);
159        }
160    }
161
162    #warn "req: ", $req->as_string, "\n";
163    #warn "DONE!!!!!!!!!!!!!!!!!!!!";
164
165    my $res = HTTP::Response->new;
166    unless ($dry_run) {
167        $res = $UserAgent->request($req);
168    }
169
170    #warn "res returned!!!";
171
172    if ($dry_run) {
173        SKIP: {
174            Test::More::skip("$name - tests skipped due to the lack of directive $dry_run", 1);
175        }
176    } else {
177        if (defined $block->error_code) {
178            is($res->code, $block->error_code, "$name - status code ok");
179        } else {
180            is($res->code, 200, "$name - status code ok");
181        }
182    }
183
184    if (defined $block->response_headers) {
185        my $headers = parse_headers($block->response_headers);
186        while (my ($key, $val) = each %$headers) {
187            my $expected_val = $res->header($key);
188            if (!defined $expected_val) {
189                $expected_val = '';
190            }
191            if ($dry_run) {
192                SKIP: {
193                    Test::More::skip("$name - tests skipped due to the lack of directive $dry_run", 1);
194                }
195            } else {
196                is $expected_val, $val,
197                    "$name - header $key ok";
198            }
199        }
200    } elsif (defined $block->response_headers_like) {
201        my $headers = parse_headers($block->response_headers_like);
202        while (my ($key, $val) = each %$headers) {
203            my $expected_val = $res->header($key);
204            if (!defined $expected_val) {
205                $expected_val = '';
206            }
207            if ($dry_run) {
208                SKIP: {
209                    Test::More::skip("$name - tests skipped due to the lack of directive $dry_run", 1);
210                }
211            } else {
212                like $expected_val, qr/^$val$/,
213                    "$name - header $key like ok";
214            }
215        }
216    }
217
218    if (defined $block->response_body) {
219        my $content = $res->content;
220        if (defined $content) {
221            $content =~ s/^TE: deflate,gzip;q=0\.3\r\n//gms;
222        }
223
224        $content =~ s/^Connection: TE, close\r\n//gms;
225        my $expected = $block->response_body;
226        $expected =~ s/\$ServerPort\b/$ServerPort/g;
227        $expected =~ s/\$ServerPortForClient\b/$ServerPortForClient/g;
228        #warn show_all_chars($content);
229
230        if ($dry_run) {
231            SKIP: {
232                Test::More::skip("$name - tests skipped due to the lack of directive $dry_run", 1);
233            }
234        } else {
235            if ($NoLongString) {
236                is($content, $expected, "$name - response_body - response is expected");
237            } else {
238                is_string($content, $expected, "$name - response_body - response is expected");
239            }
240            #is($content, $expected, "$name - response_body - response is expected");
241        }
242
243    } elsif (defined $block->response_body_like) {
244        my $content = $res->content;
245        if (defined $content) {
246            $content =~ s/^TE: deflate,gzip;q=0\.3\r\n//gms;
247        }
248        $content =~ s/^Connection: TE, close\r\n//gms;
249        my $expected_pat = $block->response_body_like;
250        $expected_pat =~ s/\$ServerPort\b/$ServerPort/g;
251        $expected_pat =~ s/\$ServerPortForClient\b/$ServerPortForClient/g;
252        my $summary = trim($content);
253
254        if ($dry_run) {
255            SKIP: {
256                Test::More::skip("$name - tests skipped due to the lack of directive $dry_run", 1);
257            }
258        } else {
259            like($content, qr/$expected_pat/s, "$name - response_body_like - response is expected ($summary)");
260        }
261    } elsif (defined $block->response_body_unlike) {
262        my $content = $res->content;
263        if (defined $content) {
264            $content =~ s/^TE: deflate,gzip;q=0\.3\r\n//gms;
265        }
266        $content =~ s/^Connection: TE, close\r\n//gms;
267        my $expected_pat = $block->response_body_unlike;
268        $expected_pat =~ s/\$ServerPort\b/$ServerPort/g;
269        $expected_pat =~ s/\$ServerPortForClient\b/$ServerPortForClient/g;
270        my $summary = trim($content);
271
272        if ($dry_run) {
273            SKIP: {
274                Test::More::skip("$name - tests skipped due to the lack of directive $dry_run", 1);
275            }
276        } else {
277           unlike($content, qr/$expected_pat/s, "$name - response_body_like - response is expected ($summary)");
278        }
279    }
280}
281
2821;
283__END__
284
285=encoding utf-8
286
287=head1 NAME
288
289Test::Nginx::LWP - LWP-backed test scaffold for the Nginx C modules
290
291=head1 SYNOPSIS
292
293    use Test::Nginx::LWP;
294
295    plan tests => $Test::Nginx::LWP::RepeatEach * 2 * blocks();
296
297    run_tests();
298
299    __DATA__
300
301    === TEST 1: sanity
302    --- config
303        location /echo {
304            echo_before_body hello;
305            echo world;
306        }
307    --- request
308        GET /echo
309    --- response_body
310    hello
311    world
312    --- error_code: 200
313
314
315    === TEST 2: set Server
316    --- config
317        location /foo {
318            echo hi;
319            more_set_headers 'Server: Foo';
320        }
321    --- request
322        GET /foo
323    --- response_headers
324    Server: Foo
325    --- response_body
326    hi
327
328
329    === TEST 3: clear Server
330    --- config
331        location /foo {
332            echo hi;
333            more_clear_headers 'Server: ';
334        }
335    --- request
336        GET /foo
337    --- response_headers_like
338    Server: nginx.*
339    --- response_body
340    hi
341
342
343    === TEST 4: set request header at client side and rewrite it
344    --- config
345        location /foo {
346            more_set_input_headers 'X-Foo: howdy';
347            echo $http_x_foo;
348        }
349    --- request
350        GET /foo
351    --- request_headers
352    X-Foo: blah
353    --- response_headers
354    X-Foo:
355    --- response_body
356    howdy
357
358
359    === TEST 3: rewrite content length
360    --- config
361        location /bar {
362            more_set_input_headers 'Content-Length: 2048';
363            echo_read_request_body;
364            echo_request_body;
365        }
366    --- request eval
367    "POST /bar\n" .
368    "a" x 4096
369    --- response_body eval
370    "a" x 2048
371
372
373    === TEST 4: timer without explicit reset
374    --- config
375        location /timer {
376            echo_sleep 0.03;
377            echo "elapsed $echo_timer_elapsed sec.";
378        }
379    --- request
380        GET /timer
381    --- response_body_like
382    ^elapsed 0\.0(2[6-9]|3[0-6]) sec\.$
383
384
385    === TEST 5: small buf (using 2-byte buf)
386    --- config
387        chunkin on;
388        location /main {
389            client_body_buffer_size    2;
390            echo "body:";
391            echo $echo_request_body;
392            echo_request_body;
393        }
394    --- request
395    POST /main
396    --- start_chunk_delay: 0.01
397    --- middle_chunk_delay: 0.01
398    --- chunked_body eval
399    ["hello", "world"]
400    --- error_code: 200
401    --- response_body eval
402    "body:
403
404    helloworld"
405
406=head1 DESCRIPTION
407
408This module provides a test scaffold based on L<LWP::UserAgent> for automated testing in Nginx C module development.
409
410This class inherits from L<Test::Base>, thus bringing all its
411declarative power to the Nginx C module testing practices.
412
413You need to terminate or kill any Nginx processes before running the test suite if you have changed the Nginx server binary. Normally it's as simple as
414
415  killall nginx
416  PATH=/path/to/your/nginx-with-memc-module:$PATH prove -r t
417
418This module will create a temporary server root under t/servroot/ of the current working directory and starts and uses the nginx executable in the PATH environment.
419
420You will often want to look into F<t/servroot/logs/error.log>
421when things go wrong ;)
422
423=head1 Sections supported
424
425The following sections are supported:
426
427=over
428
429=item config
430
431=item http_config
432
433=item request
434
435=item request_headers
436
437=item more_headers
438
439=item response_body
440
441=item response_body_like
442
443=item response_headers
444
445=item response_headers_like
446
447=item error_code
448
449=item chunked_body
450
451=item middle_chunk_delay
452
453=item start_chunk_delay
454
455=back
456
457=head1 Samples
458
459You'll find live samples in the following Nginx 3rd-party modules:
460
461=over
462
463=item ngx_echo
464
465L<http://wiki.nginx.org/NginxHttpEchoModule>
466
467=item ngx_headers_more
468
469L<http://wiki.nginx.org/NginxHttpHeadersMoreModule>
470
471=item ngx_chunkin
472
473L<http://wiki.nginx.org/NginxHttpChunkinModule>
474
475=item ngx_memc
476
477L<http://wiki.nginx.org/NginxHttpMemcModule>
478
479=back
480
481=head1 SOURCE REPOSITORY
482
483This module has a Git repository on Github, which has access for all.
484
485    http://github.com/agentzh/test-nginx
486
487If you want a commit bit, feel free to drop me a line.
488
489=head1 AUTHOR
490
491agentzh (章亦春) C<< <agentzh@gmail.com> >>
492
493=head1 COPYRIGHT & LICENSE
494
495Copyright (c) 2009-2011, Taobao Inc., Alibaba Group (L<http://www.taobao.com>).
496
497Copyright (c) 2009-2011, agentzh C<< <agentzh@gmail.com> >>.
498
499This module is licensed under the terms of the BSD license.
500
501Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
502
503=over
504
505=item *
506
507Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
508
509=item *
510
511Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
512
513=item *
514
515Neither the name of the Taobao Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
516
517=back
518
519THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
520
521=head1 SEE ALSO
522
523L<Test::Nginx::Socket>, L<Test::Base>.
524
525