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