From 096f102480124f26152fa36d9c8f6b14d40fc9c6 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Sat, 31 Oct 2020 21:09:07 -0700 Subject: [PATCH 1/6] Convert prefork.t to use subtests --- t/mojo/prefork.t | 257 +++++++++++++++++++++++++---------------------- 1 file changed, 137 insertions(+), 120 deletions(-) diff --git a/t/mojo/prefork.t b/t/mojo/prefork.t index b8b96de1e3..aca29e05b0 100644 --- a/t/mojo/prefork.t +++ b/t/mojo/prefork.t @@ -11,126 +11,143 @@ use Mojo::IOLoop::Server; use Mojo::Server::Prefork; use Mojo::UserAgent; -# Manage and clean up PID file -my $prefork = Mojo::Server::Prefork->new; -my $dir = tempdir; -ok $prefork->pid_file, 'has default path'; +my $dir = tempdir; my $file = $dir->child('prefork.pid'); -$prefork->pid_file($file); -ok !$prefork->check_pid, 'no process id'; -$prefork->ensure_pid_file(-23); -ok -e $file, 'file exists'; -is path($file)->slurp, "-23\n", 'right process id'; -ok !$prefork->check_pid, 'no process id'; -ok !-e $file, 'file has been cleaned up'; -$prefork->ensure_pid_file($$); -ok -e $file, 'file exists'; -is path($file)->slurp, "$$\n", 'right process id'; -is $prefork->check_pid, $$, 'right process id'; -undef $prefork; -ok !-e $file, 'file has been cleaned up'; - -# Bad PID file -my $bad = curfile->sibling('does_not_exist', 'test.pid'); -$prefork = Mojo::Server::Prefork->new(pid_file => $bad); -$prefork->app->log->level('debug')->unsubscribe('message'); -my $log = ''; -my $cb = $prefork->app->log->on(message => sub { $log .= pop }); -eval { $prefork->ensure_pid_file($$) }; -like $@, qr/Can't create process id file/, 'right error'; -unlike $log, qr/Creating process id file/, 'right message'; -like $log, qr/Can't create process id file/, 'right message'; -$prefork->app->log->unsubscribe(message => $cb); - -# Multiple workers and graceful shutdown -my $port = Mojo::IOLoop::Server::->generate_port; -$prefork = Mojo::Server::Prefork->new(heartbeat_interval => 0.5, listen => ["http://*:$port"], pid_file => $file); -$prefork->unsubscribe('request'); -$prefork->on( - request => sub { - my ($prefork, $tx) = @_; - $tx->res->code(200)->body('just works!'); - $tx->resume; - } -); -is $prefork->workers, 4, 'start with four workers'; -my (@spawn, @reap, $worker, $tx, $graceful); -$prefork->on(spawn => sub { push @spawn, pop }); -$prefork->on( - heartbeat => sub { - my ($prefork, $pid) = @_; - $worker = $pid; - return if $prefork->healthy < 4; - $tx = Mojo::UserAgent->new->get("http://127.0.0.1:$port"); - kill 'QUIT', $$; - } -); -$prefork->on(reap => sub { push @reap, pop }); -$prefork->on(finish => sub { $graceful = pop }); -$prefork->app->log->level('debug')->unsubscribe('message'); -$log = ''; -$cb = $prefork->app->log->on(message => sub { $log .= pop }); -is $prefork->healthy, 0, 'no healthy workers'; -my @server; -$prefork->app->hook( - before_server_start => sub { - my ($server, $app) = @_; - push @server, $server->workers, $app->mode; - } -); -$prefork->run; -is_deeply \@server, [4, 'development'], 'hook has been emitted once'; -is scalar @spawn, 4, 'four workers spawned'; -is scalar @reap, 4, 'four workers reaped'; -ok !!grep { $worker eq $_ } @spawn, 'worker has a heartbeat'; -ok $graceful, 'server has been stopped gracefully'; -is_deeply [sort @spawn], [sort @reap], 'same process ids'; -is $tx->res->code, 200, 'right status'; -is $tx->res->body, 'just works!', 'right content'; -like $log, qr/Listening at/, 'right message'; -like $log, qr/Manager $$ started/, 'right message'; -like $log, qr/Creating process id file/, 'right message'; -like $log, qr/Stopping worker $spawn[0] gracefully \(120 seconds\)/, 'right message'; -like $log, qr/Worker $spawn[0] stopped/, 'right message'; -like $log, qr/Manager $$ stopped/, 'right message'; -$prefork->app->log->unsubscribe(message => $cb); - -# Process id file -is $prefork->check_pid, $$, 'right process id'; -my $pid = $prefork->pid_file; -ok -e $pid, 'process id file has been created'; -undef $prefork; -ok !-e $pid, 'process id file has been removed'; - -# One worker and immediate shutdown -$port = Mojo::IOLoop::Server->generate_port; -$prefork - = Mojo::Server::Prefork->new(accepts => 500, heartbeat_interval => 0.5, listen => ["http://*:$port"], workers => 1); -$prefork->unsubscribe('request'); -$prefork->on( - request => sub { - my ($prefork, $tx) = @_; - $tx->res->code(200)->body('works too!'); - $tx->resume; - } -); -my $count = $tx = $graceful = undef; -@spawn = @reap = (); -$prefork->on(spawn => sub { push @spawn, pop }); -$prefork->once( - heartbeat => sub { - $tx = Mojo::UserAgent->new->get("http://127.0.0.1:$port"); - kill 'TERM', $$; - } -); -$prefork->on(reap => sub { push @reap, pop }); -$prefork->on(finish => sub { $graceful = pop }); -$prefork->run; -is $prefork->ioloop->max_accepts, 500, 'right value'; -is scalar @spawn, 1, 'one worker spawned'; -is scalar @reap, 1, 'one worker reaped'; -ok !$graceful, 'server has been stopped immediately'; -is $tx->res->code, 200, 'right status'; -is $tx->res->body, 'works too!', 'right content'; + +subtest "Manage and clean up PID file" => sub { + my $prefork = Mojo::Server::Prefork->new; + ok $prefork->pid_file, 'has default path'; + + $prefork->pid_file($file); + ok !$prefork->check_pid, 'no process id'; + + $prefork->ensure_pid_file(-23); + ok -e $file, 'file exists'; + + is path($file)->slurp, "-23\n", 'right process id'; + ok !$prefork->check_pid, 'no process id'; + ok !-e $file, 'file has been cleaned up'; + + $prefork->ensure_pid_file($$); + ok -e $file, 'file exists'; + is path($file)->slurp, "$$\n", 'right process id'; + is $prefork->check_pid, $$, 'right process id'; + + undef $prefork; + ok !-e $file, 'file has been cleaned up'; +}; + +subtest "Bad PID file" => sub { + my $bad = curfile->sibling('does_not_exist', 'test.pid'); + my $prefork = Mojo::Server::Prefork->new(pid_file => $bad); + $prefork->app->log->level('debug')->unsubscribe('message'); + my $log = ''; + my $cb = $prefork->app->log->on(message => sub { $log .= pop }); + eval { $prefork->ensure_pid_file($$) }; + like $@, qr/Can't create process id file/, 'right error'; + unlike $log, qr/Creating process id file/, 'right message'; + like $log, qr/Can't create process id file/, 'right message'; + + $prefork->app->log->unsubscribe(message => $cb); +}; + +subtest "Multiple workers and graceful shutdown" => sub { + my $port = Mojo::IOLoop::Server::->generate_port; + my $prefork = Mojo::Server::Prefork->new(heartbeat_interval => 0.5, listen => ["http://*:$port"], pid_file => $file); + $prefork->unsubscribe('request'); + $prefork->on( + request => sub { + my ($prefork, $tx) = @_; + $tx->res->code(200)->body('just works!'); + $tx->resume; + } + ); + is $prefork->workers, 4, 'start with four workers'; + + my (@spawn, @reap, $worker, $tx, $graceful); + $prefork->on(spawn => sub { push @spawn, pop }); + $prefork->on( + heartbeat => sub { + my ($prefork, $pid) = @_; + $worker = $pid; + return if $prefork->healthy < 4; + $tx = Mojo::UserAgent->new->get("http://127.0.0.1:$port"); + kill 'QUIT', $$; + } + ); + $prefork->on(reap => sub { push @reap, pop }); + $prefork->on(finish => sub { $graceful = pop }); + $prefork->app->log->level('debug')->unsubscribe('message'); + my $log = ''; + my $cb = $prefork->app->log->on(message => sub { $log .= pop }); + is $prefork->healthy, 0, 'no healthy workers'; + + my @server; + $prefork->app->hook( + before_server_start => sub { + my ($server, $app) = @_; + push @server, $server->workers, $app->mode; + } + ); + $prefork->run; + is_deeply \@server, [4, 'development'], 'hook has been emitted once'; + is scalar @spawn, 4, 'four workers spawned'; + is scalar @reap, 4, 'four workers reaped'; + ok !!grep { $worker eq $_ } @spawn, 'worker has a heartbeat'; + ok $graceful, 'server has been stopped gracefully'; + is_deeply [sort @spawn], [sort @reap], 'same process ids'; + is $tx->res->code, 200, 'right status'; + is $tx->res->body, 'just works!', 'right content'; + like $log, qr/Listening at/, 'right message'; + like $log, qr/Manager $$ started/, 'right message'; + like $log, qr/Creating process id file/, 'right message'; + like $log, qr/Stopping worker $spawn[0] gracefully \(120 seconds\)/, 'right message'; + like $log, qr/Worker $spawn[0] stopped/, 'right message'; + like $log, qr/Manager $$ stopped/, 'right message'; + + $prefork->app->log->unsubscribe(message => $cb); + + # Process id file + is $prefork->check_pid, $$, 'right process id'; + + my $pid = $prefork->pid_file; + ok -e $pid, 'process id file has been created'; + + undef $prefork; + ok !-e $pid, 'process id file has been removed'; +}; + +subtest "One worker and immediate shutdown" => sub { + my $port = Mojo::IOLoop::Server->generate_port; + my $prefork + = Mojo::Server::Prefork->new(accepts => 500, heartbeat_interval => 0.5, listen => ["http://*:$port"], workers => 1); + $prefork->unsubscribe('request'); + $prefork->on( + request => sub { + my ($prefork, $tx) = @_; + $tx->res->code(200)->body('works too!'); + $tx->resume; + } + ); + my (@spawn, @reap, $tx, $graceful); + my $count = $tx = $graceful = undef; + @spawn = @reap = (); + $prefork->on(spawn => sub { push @spawn, pop }); + $prefork->once( + heartbeat => sub { + $tx = Mojo::UserAgent->new->get("http://127.0.0.1:$port"); + kill 'TERM', $$; + } + ); + $prefork->on(reap => sub { push @reap, pop }); + $prefork->on(finish => sub { $graceful = pop }); + $prefork->run; + is $prefork->ioloop->max_accepts, 500, 'right value'; + is scalar @spawn, 1, 'one worker spawned'; + is scalar @reap, 1, 'one worker reaped'; + ok !$graceful, 'server has been stopped immediately'; + is $tx->res->code, 200, 'right status'; + is $tx->res->body, 'works too!', 'right content'; +}; done_testing(); From 0185b7ed81d3ab56b5215fbf1977ce2cbdc9d4ec Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Sat, 31 Oct 2020 21:09:10 -0700 Subject: [PATCH 2/6] Convert promise_async_await.t to use subtests --- t/mojo/promise_async_await.t | 219 ++++++++++++++++++----------------- 1 file changed, 115 insertions(+), 104 deletions(-) diff --git a/t/mojo/promise_async_await.t b/t/mojo/promise_async_await.t index d43b5feac1..abfb8425b1 100644 --- a/t/mojo/promise_async_await.t +++ b/t/mojo/promise_async_await.t @@ -17,125 +17,136 @@ use Mojo::Promise; use Mojo::UserAgent; use Mojolicious::Lite; -# async/await spec -test_awaitable('Mojo::Promise conforms to Awaitable API', class => "Mojo::Promise", force => sub { shift->wait },); +subtest "async/await spec" => sub { + test_awaitable('Mojo::Promise conforms to Awaitable API', class => "Mojo::Promise", force => sub { shift->wait },); +}; -# Silence -app->log->level('fatal'); +subtest "Silence" => sub { + app->log->level('fatal'); + + helper defer_resolve_p => sub { + my ($c, $msg) = @_; + my $promise = Mojo::Promise->new; + Mojo::IOLoop->next_tick(sub { $promise->resolve($msg) }); + return $promise; + }; + + helper defer_reject_p => sub { + my ($c, $msg) = @_; + my $promise = Mojo::Promise->new; + Mojo::IOLoop->next_tick(sub { $promise->reject($msg) }); + return $promise; + }; + + get '/one' => {text => 'works!'}; + + get '/two' => {text => 'also'}; + + get '/three' => async sub { + my $c = shift; + my $first = await $c->defer_resolve_p('this '); + my $second = await $c->defer_resolve_p('works'); + my $third = await $c->defer_resolve_p(' too!'); + $c->render(text => "$first$second$third"); + }; + + get '/four' => async sub { + my $c = shift; + + my $text = await Mojo::Promise->resolve('fail'); + eval { await $c->defer_reject_p('this went perfectly') }; + if (my $err = $@) { $c->render(text => $err, status => 500) } + else { $c->render(text => $text) } + }; + + get '/five' => async sub { + my $c = shift; + my $runaway = $c->defer_reject_p('runaway too'); + await $c->defer_resolve_p('fail'); + await $runaway; + }; + + get '/six' => sub { + my $c = shift; + $c->on( + message => async sub { + my ($c, $msg) = @_; + my $first = await $c->defer_resolve_p("One: $msg"); + my $second = await $c->defer_resolve_p("Two: $msg"); + $c->send("$first $second")->finish; + } + ); + }; + + my $ua = Mojo::UserAgent->new(ioloop => Mojo::IOLoop->singleton); + + async sub test_one { + await $ua->get_p('/one'); + } + + async sub test_two { + my $separator = shift; + + my $text = ''; + my $two = await $ua->get_p('/two'); + $text .= $two->res->body; + my $one = await $ua->get_p('/one'); + $text .= $separator . $one->res->body; + + return $text; + } + + async sub test_three { + my $ok = shift; + return Mojo::Promise->new(sub { + my ($resolve, $reject) = @_; + Mojo::IOLoop->next_tick(sub { ($ok ? $resolve : $reject)->('value') }); + }); + } + + my $t = Test::Mojo->new; -helper defer_resolve_p => sub { - my ($c, $msg) = @_; - my $promise = Mojo::Promise->new; - Mojo::IOLoop->next_tick(sub { $promise->resolve($msg) }); - return $promise; }; -helper defer_reject_p => sub { - my ($c, $msg) = @_; - my $promise = Mojo::Promise->new; - Mojo::IOLoop->next_tick(sub { $promise->reject($msg) }); - return $promise; +subtest "Basic async/await" => sub { + my $promise = test_one(); + isa_ok $promise, 'Mojo::Promise', 'right class'; + my $tx; + $promise->then(sub { $tx = shift })->catch(sub { warn @_ }); + $promise->wait; + is $tx->res->body, 'works!', 'right content'; }; -get '/one' => {text => 'works!'}; - -get '/two' => {text => 'also'}; - -get '/three' => async sub { - my $c = shift; - my $first = await $c->defer_resolve_p('this '); - my $second = await $c->defer_resolve_p('works'); - my $third = await $c->defer_resolve_p(' too!'); - $c->render(text => "$first$second$third"); +subtest "Multiple awaits" => sub { + my $text; + test_two(' ')->then(sub { $text = shift })->catch(sub { warn @_ })->wait; + is $text, 'also works!', 'right content'; }; -get '/four' => async sub { - my $c = shift; - - my $text = await Mojo::Promise->resolve('fail'); - eval { await $c->defer_reject_p('this went perfectly') }; - if (my $err = $@) { $c->render(text => $err, status => 500) } - else { $c->render(text => $text) } +subtest "Application with async/await action" => sub { + $t->get_ok('/three')->content_is('this works too!'); }; -get '/five' => async sub { - my $c = shift; - my $runaway = $c->defer_reject_p('runaway too'); - await $c->defer_resolve_p('fail'); - await $runaway; +subtest "Exception handling and async/await" => sub { + $t->get_ok('/four')->status_is(500)->content_like(qr/this went perfectly/); }; -get '/six' => sub { - my $c = shift; - $c->on( - message => async sub { - my ($c, $msg) = @_; - my $first = await $c->defer_resolve_p("One: $msg"); - my $second = await $c->defer_resolve_p("Two: $msg"); - $c->send("$first $second")->finish; - } - ); +subtest "Runaway exception" => sub { + $t->get_ok('/five')->status_is(500)->content_like(qr/runaway too/); }; -my $ua = Mojo::UserAgent->new(ioloop => Mojo::IOLoop->singleton); - -async sub test_one { - await $ua->get_p('/one'); -} - -async sub test_two { - my $separator = shift; - - my $text = ''; - my $two = await $ua->get_p('/two'); - $text .= $two->res->body; - my $one = await $ua->get_p('/one'); - $text .= $separator . $one->res->body; - - return $text; -} - -async sub test_three { - my $ok = shift; - return Mojo::Promise->new(sub { - my ($resolve, $reject) = @_; - Mojo::IOLoop->next_tick(sub { ($ok ? $resolve : $reject)->('value') }); - }); -} - -my $t = Test::Mojo->new; - -# Basic async/await -my $promise = test_one(); -isa_ok $promise, 'Mojo::Promise', 'right class'; -my $tx; -$promise->then(sub { $tx = shift })->catch(sub { warn @_ }); -$promise->wait; -is $tx->res->body, 'works!', 'right content'; - -# Multiple awaits -my $text; -test_two(' ')->then(sub { $text = shift })->catch(sub { warn @_ })->wait; -is $text, 'also works!', 'right content'; +subtest "Async function body returning a promise" => sub { + $text = undef; + test_three(1)->then(sub { $text = shift })->catch(sub { warn @_ })->wait; + is $text, 'value', 'right content'; -# Application with async/await action -$t->get_ok('/three')->content_is('this works too!'); - -# Exception handling and async/await -$t->get_ok('/four')->status_is(500)->content_like(qr/this went perfectly/); - -# Runaway exception -$t->get_ok('/five')->status_is(500)->content_like(qr/runaway too/); - -# Async function body returning a promise -$text = undef; -test_three(1)->then(sub { $text = shift })->catch(sub { warn @_ })->wait; -is $text, 'value', 'right content'; -$text = undef; -test_three(0)->then(sub { warn @_ })->catch(sub { $text = shift })->wait; -is $text, 'value', 'right content'; + $text = undef; + test_three(0)->then(sub { warn @_ })->catch(sub { $text = shift })->wait; + is $text, 'value', 'right content'; +}; -# Async WebSocket -$t->websocket_ok('/six')->send_ok('test')->message_ok->message_is('One: test Two: test')->finish_ok; +subtest "Async WebSocket" => sub { + $t->websocket_ok('/six')->send_ok('test')->message_ok->message_is('One: test Two: test')->finish_ok; +}; done_testing(); From dcc034d3d27c6a267087add19c54f9a57b0c7bce Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Sat, 31 Oct 2020 21:09:12 -0700 Subject: [PATCH 3/6] Convert response.t to use subtests --- t/mojo/response.t | 2061 ++++++++++++++++++++++++--------------------- 1 file changed, 1079 insertions(+), 982 deletions(-) diff --git a/t/mojo/response.t b/t/mojo/response.t index 2c85ef5731..6bbe4d5e02 100644 --- a/t/mojo/response.t +++ b/t/mojo/response.t @@ -9,219 +9,252 @@ use Mojo::JSON qw(encode_json); use Mojo::Message::Response; use Mojo::Util qw(encode gzip); -# Defaults -my $res = Mojo::Message::Response->new; -is $res->max_message_size, 2147483648, 'right default'; - -# Common status codes -$res = Mojo::Message::Response->new; -is $res->code(100)->default_message, 'Continue', 'right message'; -is $res->code(101)->default_message, 'Switching Protocols', 'right message'; -is $res->code(102)->default_message, 'Processing', 'right message'; -is $res->code(103)->default_message, 'Early Hints', 'right message'; -is $res->code(200)->default_message, 'OK', 'right message'; -is $res->code(201)->default_message, 'Created', 'right message'; -is $res->code(202)->default_message, 'Accepted', 'right message'; -is $res->code(203)->default_message, 'Non-Authoritative Information', 'right message'; -is $res->code(204)->default_message, 'No Content', 'right message'; -is $res->code(205)->default_message, 'Reset Content', 'right message'; -is $res->code(206)->default_message, 'Partial Content', 'right message'; -is $res->code(207)->default_message, 'Multi-Status', 'right message'; -is $res->code(208)->default_message, 'Already Reported', 'right message'; -is $res->code(226)->default_message, 'IM Used', 'right message'; -is $res->code(300)->default_message, 'Multiple Choices', 'right message'; -is $res->code(301)->default_message, 'Moved Permanently', 'right message'; -is $res->code(302)->default_message, 'Found', 'right message'; -is $res->code(303)->default_message, 'See Other', 'right message'; -is $res->code(304)->default_message, 'Not Modified', 'right message'; -is $res->code(305)->default_message, 'Use Proxy', 'right message'; -is $res->code(307)->default_message, 'Temporary Redirect', 'right message'; -is $res->code(308)->default_message, 'Permanent Redirect', 'right message'; -is $res->code(400)->default_message, 'Bad Request', 'right message'; -is $res->code(401)->default_message, 'Unauthorized', 'right message'; -is $res->code(402)->default_message, 'Payment Required', 'right message'; -is $res->code(403)->default_message, 'Forbidden', 'right message'; -is $res->code(404)->default_message, 'Not Found', 'right message'; -is $res->code(405)->default_message, 'Method Not Allowed', 'right message'; -is $res->code(406)->default_message, 'Not Acceptable', 'right message'; -is $res->code(407)->default_message, 'Proxy Authentication Required', 'right message'; -is $res->code(408)->default_message, 'Request Timeout', 'right message'; -is $res->code(409)->default_message, 'Conflict', 'right message'; -is $res->code(410)->default_message, 'Gone', 'right message'; -is $res->code(411)->default_message, 'Length Required', 'right message'; -is $res->code(412)->default_message, 'Precondition Failed', 'right message'; -is $res->code(413)->default_message, 'Request Entity Too Large', 'right message'; -is $res->code(414)->default_message, 'Request-URI Too Long', 'right message'; -is $res->code(415)->default_message, 'Unsupported Media Type', 'right message'; -is $res->code(416)->default_message, 'Request Range Not Satisfiable', 'right message'; -is $res->code(417)->default_message, 'Expectation Failed', 'right message'; -is $res->code(418)->default_message, "I'm a teapot", 'right message'; -is $res->code(421)->default_message, 'Misdirected Request', 'right message'; -is $res->code(422)->default_message, 'Unprocessable Entity', 'right message'; -is $res->code(423)->default_message, 'Locked', 'right message'; -is $res->code(424)->default_message, 'Failed Dependency', 'right message'; -is $res->code(425)->default_message, 'Too Early', 'right message'; -is $res->code(426)->default_message, 'Upgrade Required', 'right message'; -is $res->code(428)->default_message, 'Precondition Required', 'right message'; -is $res->code(429)->default_message, 'Too Many Requests', 'right message'; -is $res->code(431)->default_message, 'Request Header Fields Too Large', 'right message'; -is $res->code(451)->default_message, 'Unavailable For Legal Reasons', 'right message'; -is $res->code(500)->default_message, 'Internal Server Error', 'right message'; -is $res->code(501)->default_message, 'Not Implemented', 'right message'; -is $res->code(502)->default_message, 'Bad Gateway', 'right message'; -is $res->code(503)->default_message, 'Service Unavailable', 'right message'; -is $res->code(504)->default_message, 'Gateway Timeout', 'right message'; -is $res->code(505)->default_message, 'HTTP Version Not Supported', 'right message'; -is $res->code(506)->default_message, 'Variant Also Negotiates', 'right message'; -is $res->code(507)->default_message, 'Insufficient Storage', 'right message'; -is $res->code(508)->default_message, 'Loop Detected', 'right message'; -is $res->code(509)->default_message, 'Bandwidth Limit Exceeded', 'right message'; -is $res->code(510)->default_message, 'Not Extended', 'right message'; -is $res->code(511)->default_message, 'Network Authentication Required', 'right message'; -is $res->default_message(100), 'Continue', 'right message'; - -# Status code ranges -ok $res->code(101)->is_info, 'is in range'; -ok $res->code(199)->is_info, 'is in range'; -ok $res->code(200)->is_success, 'is in range'; -ok $res->code(202)->is_success, 'is in range'; -ok $res->code(299)->is_success, 'is in range'; -ok $res->code(301)->is_redirect, 'is in range'; -ok $res->code(399)->is_redirect, 'is in range'; -ok $res->code(401)->is_client_error, 'is in range'; -ok $res->code(499)->is_client_error, 'is in range'; -ok $res->code(400)->is_error, 'is in range'; -ok $res->code(599)->is_error, 'is in range'; -ok $res->code(501)->is_server_error, 'is in range'; -ok $res->code(599)->is_server_error, 'is in range'; -ok !$res->code(200)->is_info, 'not in range'; -ok !$res->code(199)->is_success, 'not in range'; -ok !$res->code(300)->is_success, 'not in range'; -ok !$res->code(200)->is_redirect, 'not in range'; -ok !$res->code(200)->is_error, 'not in range'; -ok !$res->code(200)->is_client_error, 'not in range'; -ok !$res->code(200)->is_server_error, 'not in range'; -ok !$res->code(undef)->is_success, 'no range'; - -# Status code and message -$res = Mojo::Message::Response->new; -is $res->code, undef, 'no status'; -is $res->default_message, 'Not Found', 'right default message'; -is $res->message, undef, 'no message'; -$res->message('Test'); -is $res->message, 'Test', 'right message'; -$res->code(500); -is $res->code, 500, 'right status'; -is $res->message, 'Test', 'right message'; -is $res->default_message, 'Internal Server Error', 'right default message'; -$res = Mojo::Message::Response->new; -is $res->code(400)->default_message, 'Bad Request', 'right default message'; -$res = Mojo::Message::Response->new; -is $res->code(1)->default_message, '', 'empty default message'; - -# Parse HTTP 1.1 response start-line, no headers and body -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0d\x0a\x0d\x0a"); -ok !$res->is_finished, 'response is not finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; - -# Parse HTTP 1.1 response start-line, no headers and body (small chunks) -$res = Mojo::Message::Response->new; -$res->parse('H'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('T'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('T'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('P'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('/'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('1'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('.'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('1'); -ok !$res->is_finished, 'response is not finished'; -$res->parse(' '); -ok !$res->is_finished, 'response is not finished'; -$res->parse('2'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('0'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('0'); -ok !$res->is_finished, 'response is not finished'; -$res->parse(' '); -ok !$res->is_finished, 'response is not finished'; -$res->parse('O'); -ok !$res->is_finished, 'response is not finished'; -$res->parse('K'); -ok !$res->is_finished, 'response is not finished'; -$res->parse("\x0d"); -ok !$res->is_finished, 'response is not finished'; -$res->parse("\x0a"); -ok !$res->is_finished, 'response is not finished'; -$res->parse("\x0d"); -ok !$res->is_finished, 'response is not finished'; -$res->parse("\x0a"); -ok !$res->is_finished, 'response is not finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; - -# Parse HTTP 1.1 response start-line, no headers and body (no message) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200\x0d\x0a\x0d\x0a"); -ok !$res->is_finished, 'response is not finished'; -is $res->code, 200, 'right status'; -is $res->message, undef, 'no message'; -is $res->version, '1.1', 'right version'; - -# Parse HTTP 1.0 response start-line and headers but no body -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.0 404 Damn it\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Content-Length: 0\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -is $res->code, 404, 'right status'; -is $res->message, 'Damn it', 'right message'; -is $res->version, '1.0', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, 0, 'right "Content-Length" value'; - -# Parse full HTTP 1.0 response -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Content-Length: 27\x0d\x0a\x0d\x0a"); -$res->parse("Hello World!\n1234\nlalalala\n"); -ok $res->is_finished, 'response is finished'; -is $res->code, 500, 'right status'; -is $res->message, 'Internal Server Error', 'right message'; -is $res->version, '1.0', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, 27, 'right "Content-Length" value'; -is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; - -# Parse full HTTP 1.0 response (keep-alive) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); -$res->parse("Connection: keep-alive\x0d\x0a\x0d\x0a"); -$res->parse("HTTP/1.0 200 OK\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -is $res->code, 500, 'right status'; -is $res->message, 'Internal Server Error', 'right message'; -is $res->version, '1.0', 'right version'; -is $res->body, '', 'no content'; -is $res->content->leftovers, "HTTP/1.0 200 OK\x0d\x0a\x0d\x0a", 'next response in leftovers'; - -# Parse full HTTP 1.0 response (no limit) -{ - local $ENV{MOJO_MAX_MESSAGE_SIZE} = 0; +subtest "Defaults" => sub { + my $res = Mojo::Message::Response->new; + is $res->max_message_size, 2147483648, 'right default'; +}; + +subtest "Common status codes" => sub { + my $res = Mojo::Message::Response->new; + is $res->code(100)->default_message, 'Continue', 'right message'; + is $res->code(101)->default_message, 'Switching Protocols', 'right message'; + is $res->code(102)->default_message, 'Processing', 'right message'; + is $res->code(103)->default_message, 'Early Hints', 'right message'; + is $res->code(200)->default_message, 'OK', 'right message'; + is $res->code(201)->default_message, 'Created', 'right message'; + is $res->code(202)->default_message, 'Accepted', 'right message'; + is $res->code(203)->default_message, 'Non-Authoritative Information', 'right message'; + is $res->code(204)->default_message, 'No Content', 'right message'; + is $res->code(205)->default_message, 'Reset Content', 'right message'; + is $res->code(206)->default_message, 'Partial Content', 'right message'; + is $res->code(207)->default_message, 'Multi-Status', 'right message'; + is $res->code(208)->default_message, 'Already Reported', 'right message'; + is $res->code(226)->default_message, 'IM Used', 'right message'; + is $res->code(300)->default_message, 'Multiple Choices', 'right message'; + is $res->code(301)->default_message, 'Moved Permanently', 'right message'; + is $res->code(302)->default_message, 'Found', 'right message'; + is $res->code(303)->default_message, 'See Other', 'right message'; + is $res->code(304)->default_message, 'Not Modified', 'right message'; + is $res->code(305)->default_message, 'Use Proxy', 'right message'; + is $res->code(307)->default_message, 'Temporary Redirect', 'right message'; + is $res->code(308)->default_message, 'Permanent Redirect', 'right message'; + is $res->code(400)->default_message, 'Bad Request', 'right message'; + is $res->code(401)->default_message, 'Unauthorized', 'right message'; + is $res->code(402)->default_message, 'Payment Required', 'right message'; + is $res->code(403)->default_message, 'Forbidden', 'right message'; + is $res->code(404)->default_message, 'Not Found', 'right message'; + is $res->code(405)->default_message, 'Method Not Allowed', 'right message'; + is $res->code(406)->default_message, 'Not Acceptable', 'right message'; + is $res->code(407)->default_message, 'Proxy Authentication Required', 'right message'; + is $res->code(408)->default_message, 'Request Timeout', 'right message'; + is $res->code(409)->default_message, 'Conflict', 'right message'; + is $res->code(410)->default_message, 'Gone', 'right message'; + is $res->code(411)->default_message, 'Length Required', 'right message'; + is $res->code(412)->default_message, 'Precondition Failed', 'right message'; + is $res->code(413)->default_message, 'Request Entity Too Large', 'right message'; + is $res->code(414)->default_message, 'Request-URI Too Long', 'right message'; + is $res->code(415)->default_message, 'Unsupported Media Type', 'right message'; + is $res->code(416)->default_message, 'Request Range Not Satisfiable', 'right message'; + is $res->code(417)->default_message, 'Expectation Failed', 'right message'; + is $res->code(418)->default_message, "I'm a teapot", 'right message'; + is $res->code(421)->default_message, 'Misdirected Request', 'right message'; + is $res->code(422)->default_message, 'Unprocessable Entity', 'right message'; + is $res->code(423)->default_message, 'Locked', 'right message'; + is $res->code(424)->default_message, 'Failed Dependency', 'right message'; + is $res->code(425)->default_message, 'Too Early', 'right message'; + is $res->code(426)->default_message, 'Upgrade Required', 'right message'; + is $res->code(428)->default_message, 'Precondition Required', 'right message'; + is $res->code(429)->default_message, 'Too Many Requests', 'right message'; + is $res->code(431)->default_message, 'Request Header Fields Too Large', 'right message'; + is $res->code(451)->default_message, 'Unavailable For Legal Reasons', 'right message'; + is $res->code(500)->default_message, 'Internal Server Error', 'right message'; + is $res->code(501)->default_message, 'Not Implemented', 'right message'; + is $res->code(502)->default_message, 'Bad Gateway', 'right message'; + is $res->code(503)->default_message, 'Service Unavailable', 'right message'; + is $res->code(504)->default_message, 'Gateway Timeout', 'right message'; + is $res->code(505)->default_message, 'HTTP Version Not Supported', 'right message'; + is $res->code(506)->default_message, 'Variant Also Negotiates', 'right message'; + is $res->code(507)->default_message, 'Insufficient Storage', 'right message'; + is $res->code(508)->default_message, 'Loop Detected', 'right message'; + is $res->code(509)->default_message, 'Bandwidth Limit Exceeded', 'right message'; + is $res->code(510)->default_message, 'Not Extended', 'right message'; + is $res->code(511)->default_message, 'Network Authentication Required', 'right message'; + is $res->default_message(100), 'Continue', 'right message'; +}; + +subtest "Status code ranges" => sub { + my $res = Mojo::Message::Response->new; + ok $res->code(101)->is_info, 'is in range'; + ok $res->code(199)->is_info, 'is in range'; + ok $res->code(200)->is_success, 'is in range'; + ok $res->code(202)->is_success, 'is in range'; + ok $res->code(299)->is_success, 'is in range'; + ok $res->code(301)->is_redirect, 'is in range'; + ok $res->code(399)->is_redirect, 'is in range'; + ok $res->code(401)->is_client_error, 'is in range'; + ok $res->code(499)->is_client_error, 'is in range'; + ok $res->code(400)->is_error, 'is in range'; + ok $res->code(599)->is_error, 'is in range'; + ok $res->code(501)->is_server_error, 'is in range'; + ok $res->code(599)->is_server_error, 'is in range'; + ok !$res->code(200)->is_info, 'not in range'; + ok !$res->code(199)->is_success, 'not in range'; + ok !$res->code(300)->is_success, 'not in range'; + ok !$res->code(200)->is_redirect, 'not in range'; + ok !$res->code(200)->is_error, 'not in range'; + ok !$res->code(200)->is_client_error, 'not in range'; + ok !$res->code(200)->is_server_error, 'not in range'; + ok !$res->code(undef)->is_success, 'no range'; +}; + +subtest "Status code and message" => sub { + my $res = Mojo::Message::Response->new; + is $res->code, undef, 'no status'; + is $res->default_message, 'Not Found', 'right default message'; + is $res->message, undef, 'no message'; + + $res->message('Test'); + is $res->message, 'Test', 'right message'; + + $res->code(500); + is $res->code, 500, 'right status'; + is $res->message, 'Test', 'right message'; + is $res->default_message, 'Internal Server Error', 'right default message'; + $res = Mojo::Message::Response->new; + is $res->code(400)->default_message, 'Bad Request', 'right default message'; + + $res = Mojo::Message::Response->new; + is $res->code(1)->default_message, '', 'empty default message'; +}; + +subtest "Parse HTTP 1.1 response start-line, no headers and body" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0d\x0a\x0d\x0a"); + ok !$res->is_finished, 'response is not finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; +}; + +subtest "Parse HTTP 1.1 response start-line, no headers and body (small chunks)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse('H'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('T'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('T'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('P'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('/'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('1'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('.'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('1'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse(' '); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('2'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('0'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('0'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse(' '); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('O'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse('K'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse("\x0d"); + ok !$res->is_finished, 'response is not finished'; + + $res->parse("\x0a"); + ok !$res->is_finished, 'response is not finished'; + + $res->parse("\x0d"); + ok !$res->is_finished, 'response is not finished'; + + $res->parse("\x0a"); + ok !$res->is_finished, 'response is not finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; +}; + +subtest "Parse HTTP 1.1 response start-line, no headers and body (no message)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200\x0d\x0a\x0d\x0a"); + ok !$res->is_finished, 'response is not finished'; + is $res->code, 200, 'right status'; + is $res->message, undef, 'no message'; + is $res->version, '1.1', 'right version'; +}; + +subtest "Parse HTTP 1.0 response start-line and headers but no body" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.0 404 Damn it\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Content-Length: 0\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + is $res->code, 404, 'right status'; + is $res->message, 'Damn it', 'right message'; + is $res->version, '1.0', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, 0, 'right "Content-Length" value'; +}; + +subtest "Parse full HTTP 1.0 response" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Content-Length: 27\x0d\x0a\x0d\x0a"); + $res->parse("Hello World!\n1234\nlalalala\n"); + ok $res->is_finished, 'response is finished'; + is $res->code, 500, 'right status'; + is $res->message, 'Internal Server Error', 'right message'; + is $res->version, '1.0', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, 27, 'right "Content-Length" value'; + is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; +}; + +subtest "Parse full HTTP 1.0 response (keep-alive)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); + $res->parse("Connection: keep-alive\x0d\x0a\x0d\x0a"); + $res->parse("HTTP/1.0 200 OK\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + is $res->code, 500, 'right status'; + is $res->message, 'Internal Server Error', 'right message'; + is $res->version, '1.0', 'right version'; + is $res->body, '', 'no content'; + is $res->content->leftovers, "HTTP/1.0 200 OK\x0d\x0a\x0d\x0a", 'next response in leftovers'; +}; + +subtest "Parse full HTTP 1.0 response (no limit)" => sub { + local $ENV{MOJO_MAX_MESSAGE_SIZE} = 0; + my $res = Mojo::Message::Response->new; is $res->max_message_size, 0, 'right size'; + $res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); $res->parse("Content-Type: text/plain\x0d\x0a"); $res->parse("Content-Length: 27\x0d\x0a\x0d\x0a"); @@ -234,150 +267,160 @@ is $res->content->leftovers, "HTTP/1.0 200 OK\x0d\x0a\x0d\x0a", 'next response i is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; is $res->headers->content_length, 27, 'right "Content-Length" value'; is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; -} - -# Parse broken start-line -$res = Mojo::Message::Response->new; -$res->parse("12345\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -is $res->error->{message}, 'Bad response start-line', 'right error'; - -# Parse full HTTP 1.0 response (missing Content-Length) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Connection: close\x0d\x0a\x0d\x0a"); -$res->parse("Hello World!\n1234\nlalalala\n"); -ok !$res->is_finished, 'response is not finished'; -is $res->code, 500, 'right status'; -is $res->message, 'Internal Server Error', 'right message'; -is $res->version, '1.0', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, undef, 'no "Content-Length" value'; -is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; - -# Parse full HTTP 1.0 response (missing Content-Length and Connection) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a\x0d\x0a"); -$res->parse("Hello World!\n1"); -$res->parse("234\nlala"); -$res->parse("lala\n"); -ok !$res->is_finished, 'response is not finished'; -is $res->code, 500, 'right status'; -is $res->message, 'Internal Server Error', 'right message'; -is $res->version, '1.0', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, undef, 'no "Content-Length" value'; -is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; - -# Parse full HTTP 1.1 response (missing Content-Length) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 500 Internal Server Error\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Connection: close\x0d\x0a\x0d\x0a"); -$res->parse("Hello World!\n1234\nlalalala\n"); -ok !$res->is_finished, 'response is not finished'; -ok !$res->is_empty, 'response is not empty'; -ok !$res->content->skip_body, 'body has not been skipped'; -ok $res->content->relaxed, 'relaxed response'; -is $res->code, 500, 'right status'; -is $res->message, 'Internal Server Error', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, undef, 'no "Content-Length" value'; -is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; - -# Parse full HTTP 1.1 response (broken Content-Length) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0d\x0a"); -$res->parse("Content-Length: 123test\x0d\x0a\x0d\x0a"); -$res->parse('Hello World!'); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_length, '123test', 'right "Content-Length" value'; -is $res->body, '', 'no content'; -is $res->content->leftovers, 'Hello World!', 'content in leftovers'; - -# Parse full HTTP 1.1 response (100 Continue) -$res = Mojo::Message::Response->new; -$res->content->on(body => sub { shift->headers->header('X-Body' => 'one') }); -$res->on(progress => sub { shift->headers->header('X-Progress' => 'two') }); -$res->on(finish => sub { shift->headers->header('X-Finish' => 'three') }); -$res->parse("HTTP/1.1 100 Continue\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -ok $res->is_empty, 'response is empty'; -ok $res->content->skip_body, 'body has been skipped'; -is $res->code, 100, 'right status'; -is $res->message, 'Continue', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_length, undef, 'no "Content-Length" value'; -is $res->headers->header('X-Body'), 'one', 'right "X-Body" value'; -is $res->headers->header('X-Progress'), 'two', 'right "X-Progress" value'; -is $res->headers->header('X-Finish'), 'three', 'right "X-Finish" value'; -is $res->body, '', 'no content'; - -# Parse full HTTP 1.1 response (304 Not Modified) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 304 Not Modified\x0d\x0a"); -$res->parse("Content-Type: text/html\x0d\x0a"); -$res->parse("Content-Length: 9000\x0d\x0a"); -$res->parse("Connection: keep-alive\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -ok $res->is_empty, 'response is empty'; -ok $res->content->skip_body, 'body has been skipped'; -is $res->code, 304, 'right status'; -is $res->message, 'Not Modified', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'text/html', 'right "Content-Type" value'; -is $res->headers->content_length, 9000, 'right "Content-Length" value'; -is $res->headers->connection, 'keep-alive', 'right "Connection" value'; -is $res->body, '', 'no content'; - -# Parse full HTTP 1.1 response (204 No Content) -$res = Mojo::Message::Response->new; -$res->content->on(body => sub { shift->headers->header('X-Body' => 'one') }); -$res->on(finish => sub { shift->headers->header('X-Finish' => 'two') }); -$res->parse("HTTP/1.1 204 No Content\x0d\x0a"); -$res->parse("Content-Type: text/html\x0d\x0a"); -$res->parse("Content-Length: 9001\x0d\x0a"); -$res->parse("Connection: keep-alive\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -ok $res->is_empty, 'response is empty'; -ok $res->content->skip_body, 'body has been skipped'; -is $res->code, 204, 'right status'; -is $res->message, 'No Content', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'text/html', 'right "Content-Type" value'; -is $res->headers->content_length, 9001, 'right "Content-Length" value'; -is $res->headers->connection, 'keep-alive', 'right "Connection" value'; -is $res->headers->header('X-Body'), 'one', 'right "X-Body" value'; -is $res->headers->header('X-Finish'), 'two', 'right "X-Finish" value'; -is $res->body, '', 'no content'; - -# Parse HTTP 1.1 response (413 error in one big chunk) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 413 Request Entity Too Large\x0d\x0a" - . "Connection: Close\x0d\x0a" - . "Date: Tue, 09 Feb 2010 16:34:51 GMT\x0d\x0a" - . "Server: Mojolicious (Perl)\x0d\x0a\x0d\x0a"); -ok !$res->is_finished, 'response is not finished'; -is $res->code, 413, 'right status'; -is $res->message, 'Request Entity Too Large', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_length, undef, 'right "Content-Length" value'; - -# Parse HTTP 1.1 chunked response (exceeding limit) -{ +}; + +subtest "Parse broken start-line" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("12345\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + is $res->error->{message}, 'Bad response start-line', 'right error'; +}; + +subtest "Parse full HTTP 1.0 response (missing Content-Length)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Connection: close\x0d\x0a\x0d\x0a"); + $res->parse("Hello World!\n1234\nlalalala\n"); + ok !$res->is_finished, 'response is not finished'; + is $res->code, 500, 'right status'; + is $res->message, 'Internal Server Error', 'right message'; + is $res->version, '1.0', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, undef, 'no "Content-Length" value'; + is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; +}; + +subtest "Parse full HTTP 1.0 response (missing Content-Length and Connection)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.0 500 Internal Server Error\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a\x0d\x0a"); + $res->parse("Hello World!\n1"); + $res->parse("234\nlala"); + $res->parse("lala\n"); + ok !$res->is_finished, 'response is not finished'; + is $res->code, 500, 'right status'; + is $res->message, 'Internal Server Error', 'right message'; + is $res->version, '1.0', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, undef, 'no "Content-Length" value'; + is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; +}; + +subtest "Parse full HTTP 1.1 response (missing Content-Length)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 500 Internal Server Error\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Connection: close\x0d\x0a\x0d\x0a"); + $res->parse("Hello World!\n1234\nlalalala\n"); + ok !$res->is_finished, 'response is not finished'; + ok !$res->is_empty, 'response is not empty'; + ok !$res->content->skip_body, 'body has not been skipped'; + ok $res->content->relaxed, 'relaxed response'; + is $res->code, 500, 'right status'; + is $res->message, 'Internal Server Error', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, undef, 'no "Content-Length" value'; + is $res->body, "Hello World!\n1234\nlalalala\n", 'right content'; +}; + +subtest "Parse full HTTP 1.1 response (broken Content-Length)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); + $res->parse("Content-Length: 123test\x0d\x0a\x0d\x0a"); + $res->parse('Hello World!'); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_length, '123test', 'right "Content-Length" value'; + is $res->body, '', 'no content'; + is $res->content->leftovers, 'Hello World!', 'content in leftovers'; +}; + +subtest "Parse full HTTP 1.1 response (100 Continue)" => sub { + my $res = Mojo::Message::Response->new; + $res->content->on(body => sub { shift->headers->header('X-Body' => 'one') }); + $res->on(progress => sub { shift->headers->header('X-Progress' => 'two') }); + $res->on(finish => sub { shift->headers->header('X-Finish' => 'three') }); + $res->parse("HTTP/1.1 100 Continue\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + ok $res->is_empty, 'response is empty'; + ok $res->content->skip_body, 'body has been skipped'; + is $res->code, 100, 'right status'; + is $res->message, 'Continue', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_length, undef, 'no "Content-Length" value'; + is $res->headers->header('X-Body'), 'one', 'right "X-Body" value'; + is $res->headers->header('X-Progress'), 'two', 'right "X-Progress" value'; + is $res->headers->header('X-Finish'), 'three', 'right "X-Finish" value'; + is $res->body, '', 'no content'; +}; + +subtest "Parse full HTTP 1.1 response (304 Not Modified)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 304 Not Modified\x0d\x0a"); + $res->parse("Content-Type: text/html\x0d\x0a"); + $res->parse("Content-Length: 9000\x0d\x0a"); + $res->parse("Connection: keep-alive\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + ok $res->is_empty, 'response is empty'; + ok $res->content->skip_body, 'body has been skipped'; + is $res->code, 304, 'right status'; + is $res->message, 'Not Modified', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'text/html', 'right "Content-Type" value'; + is $res->headers->content_length, 9000, 'right "Content-Length" value'; + is $res->headers->connection, 'keep-alive', 'right "Connection" value'; + is $res->body, '', 'no content'; +}; + +subtest "Parse full HTTP 1.1 response (204 No Content)" => sub { + my $res = Mojo::Message::Response->new; + $res->content->on(body => sub { shift->headers->header('X-Body' => 'one') }); + $res->on(finish => sub { shift->headers->header('X-Finish' => 'two') }); + $res->parse("HTTP/1.1 204 No Content\x0d\x0a"); + $res->parse("Content-Type: text/html\x0d\x0a"); + $res->parse("Content-Length: 9001\x0d\x0a"); + $res->parse("Connection: keep-alive\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + ok $res->is_empty, 'response is empty'; + ok $res->content->skip_body, 'body has been skipped'; + is $res->code, 204, 'right status'; + is $res->message, 'No Content', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'text/html', 'right "Content-Type" value'; + is $res->headers->content_length, 9001, 'right "Content-Length" value'; + is $res->headers->connection, 'keep-alive', 'right "Connection" value'; + is $res->headers->header('X-Body'), 'one', 'right "X-Body" value'; + is $res->headers->header('X-Finish'), 'two', 'right "X-Finish" value'; + is $res->body, '', 'no content'; +}; + +subtest "Parse HTTP 1.1 response (413 error in one big chunk)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 413 Request Entity Too Large\x0d\x0a" + . "Connection: Close\x0d\x0a" + . "Date: Tue, 09 Feb 2010 16:34:51 GMT\x0d\x0a" + . "Server: Mojolicious (Perl)\x0d\x0a\x0d\x0a"); + ok !$res->is_finished, 'response is not finished'; + is $res->code, 413, 'right status'; + is $res->message, 'Request Entity Too Large', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_length, undef, 'right "Content-Length" value'; +}; + +subtest "Parse HTTP 1.1 chunked response (exceeding limit)" => sub { local $ENV{MOJO_MAX_BUFFER_SIZE} = 12; - $res = Mojo::Message::Response->new; + my $res = Mojo::Message::Response->new; is $res->content->max_buffer_size, 12, 'right size'; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); $res->parse("Content-Type: text/plain\x0d\x0a"); $res->parse("Transfer-Encoding: chunked\x0d\x0a\x0d\x0a"); ok !$res->is_limit_exceeded, 'limit is not exceeded'; + $res->parse('a' x 1000); ok $res->is_finished, 'response is finished'; ok $res->content->is_finished, 'content is finished'; @@ -387,18 +430,19 @@ is $res->headers->content_length, undef, 'right "Content-Length" value'; is $res->message, 'OK', 'right message'; is $res->version, '1.1', 'right version'; is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -} +}; -# Parse HTTP 1.1 multipart response (exceeding limit) -{ +subtest "Parse HTTP 1.1 multipart response (exceeding limit)" => sub { local $ENV{MOJO_MAX_BUFFER_SIZE} = 12; - $res = Mojo::Message::Response->new; + my $res = Mojo::Message::Response->new; is $res->content->max_buffer_size, 12, 'right size'; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); $res->parse("Content-Length: 420\x0d\x0a"); $res->parse('Content-Type: multipart/form-data; bo'); $res->parse("undary=----------0xKhTmLbOuNdArY\x0d\x0a\x0d\x0a"); ok !$res->content->is_limit_exceeded, 'limit is not exceeded'; + $res->parse('a' x 200); ok $res->content->is_limit_exceeded, 'limit is exceeded'; ok $res->is_finished, 'response is finished'; @@ -409,18 +453,19 @@ is $res->headers->content_length, undef, 'right "Content-Length" value'; is $res->version, '1.1', 'right version'; is $res->headers->content_type, 'multipart/form-data; boundary=----------0xKhTmLbOuNdArY', 'right "Content-Type" value'; -} +}; -# Parse HTTP 1.1 gzip compressed response (garbage bytes exceeding limit) -{ +subtest "Parse HTTP 1.1 gzip compressed response (garbage bytes exceeding limit)" => sub { local $ENV{MOJO_MAX_BUFFER_SIZE} = 12; - $res = Mojo::Message::Response->new; + my $res = Mojo::Message::Response->new; is $res->content->max_buffer_size, 12, 'right size'; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); $res->parse("Content-Length: 1000\x0d\x0a"); $res->parse("Content-Encoding: gzip\x0d\x0a\x0d\x0a"); $res->parse('a' x 5); ok !$res->content->is_limit_exceeded, 'limit is not exceeded'; + $res->parse('a' x 995); ok $res->content->is_limit_exceeded, 'limit is exceeded'; ok $res->is_finished, 'response is finished'; @@ -430,638 +475,690 @@ is $res->headers->content_length, undef, 'right "Content-Length" value'; is $res->message, 'OK', 'right message'; is $res->version, '1.1', 'right version'; is $res->body, '', 'no content'; -} - -# Parse HTTP 1.1 chunked response -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 500 Internal Server Error\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Transfer-Encoding: chunked\x0d\x0a\x0d\x0a"); -$res->parse("4\x0d\x0a"); -$res->parse("abcd\x0d\x0a"); -$res->parse("9\x0d\x0a"); -$res->parse("abcdefghi\x0d\x0a"); -$res->parse("0\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -is $res->code, 500, 'right status'; -is $res->message, 'Internal Server Error', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, 13, 'right "Content-Length" value'; -is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; -is $res->body_size, 13, 'right size'; - -# Parse HTTP 1.1 multipart response -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0d\x0a"); -$res->parse("Content-Length: 420\x0d\x0a"); -$res->parse('Content-Type: multipart/form-data; bo'); -$res->parse("undary=----------0xKhTmLbOuNdArY\x0d\x0a\x0d\x0a"); -$res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); -$res->parse("Content-Disposition: form-data; name=\"text1\"\x0d\x0a"); -$res->parse("\x0d\x0ahallo welt test123\n"); -$res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); -$res->parse("Content-Disposition: form-data; name=\"text2\"\x0d\x0a"); -$res->parse("\x0d\x0a\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); -$res->parse('Content-Disposition: form-data; name="upload"; file'); -$res->parse("name=\"hello.pl\"\x0d\x0a\x0d\x0a"); -$res->parse("Content-Type: application/octet-stream\x0d\x0a\x0d\x0a"); -$res->parse("#!/usr/bin/perl\n\n"); -$res->parse("use strict;\n"); -$res->parse("use warnings;\n\n"); -$res->parse("print \"Hello World :)\\n\"\n"); -$res->parse("\x0d\x0a------------0xKhTmLbOuNdArY--"); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'multipart/form-data; boundary=----------0xKhTmLbOuNdArY', 'right "Content-Type" value'; -ok !$res->content->parts->[0]->is_multipart, 'no multipart content'; -ok !$res->content->parts->[1]->is_multipart, 'no multipart content'; -ok !$res->content->parts->[2]->is_multipart, 'no multipart content'; -is $res->content->parts->[0]->asset->slurp, "hallo welt test123\n", 'right content'; -my $dir = tempdir; -my $file = $dir->child('multipart.html'); -eval { $res->save_to($file) }; -like $@, qr/^Multipart content cannot be saved to files/, 'right error'; - -# Parse HTTP 1.1 chunked multipart response with leftovers (at once) -$res = Mojo::Message::Response->new; -my $multipart - = "HTTP/1.1 200 OK\x0d\x0a" - . "Transfer-Encoding: chunked\x0d\x0a" - . 'Content-Type: multipart/form-data; bo' - . "undary=----------0xKhTmLbOuNdArY\x0d\x0a\x0d\x0a" - . "19f\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a" - . "Content-Disposition: form-data; name=\"text1\"\x0d\x0a" - . "\x0d\x0ahallo welt test123\n" - . "\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a" - . "Content-Disposition: form-data; name=\"text2\"\x0d\x0a" - . "\x0d\x0a\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a" - . 'Content-Disposition: form-data; name="upload"; file' - . "name=\"hello.pl\"\x0d\x0a" - . "Content-Type: application/octet-stream\x0d\x0a\x0d\x0a" - . "#!/usr/bin/perl\n\n" - . "use strict;\n" - . "use warnings;\n\n" - . "print \"Hello World :)\\n\"\n" - . "\x0d\x0a------------0xKhTmLbOuNdA" - . "r\x0d\x0a3\x0d\x0aY--\x0d\x0a" - . "0\x0d\x0a\x0d\x0a" - . "HTTP/1.0 200 OK\x0d\x0a\x0d\x0a"; -$res->parse($multipart); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'multipart/form-data; boundary=----------0xKhTmLbOuNdArY', 'right "Content-Type" value'; -is $res->headers->content_length, 418, 'right "Content-Length" value'; -is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; -is $res->body_size, 418, 'right size'; -ok !$res->content->parts->[0]->is_multipart, 'no multipart content'; -ok !$res->content->parts->[1]->is_multipart, 'no multipart content'; -ok !$res->content->parts->[2]->is_multipart, 'no multipart content'; -is $res->content->parts->[0]->asset->slurp, "hallo welt test123\n", 'right content'; -is $res->upload('upload')->filename, 'hello.pl', 'right filename'; -ok !$res->upload('upload')->asset->is_file, 'stored in memory'; -is $res->upload('upload')->asset->size, 69, 'right size'; -is $res->content->parts->[2]->headers->content_type, 'application/octet-stream', 'right "Content-Type" value'; -is $res->content->leftovers, "HTTP/1.0 200 OK\x0d\x0a\x0d\x0a", 'next response in leftovers'; - -# Parse HTTP 1.1 chunked multipart response (in multiple small chunks) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0d\x0a"); -$res->parse("Transfer-Encoding: chunked\x0d\x0a"); -$res->parse('Content-Type: multipart/parallel; boundary=AAA; charset=utf-8'); -$res->parse("\x0d\x0a\x0d\x0a"); -$res->parse("7\x0d\x0a"); -$res->parse("--AAA\x0d\x0a"); -$res->parse("\x0d\x0a1a\x0d\x0a"); -$res->parse("Content-Type: image/jpeg\x0d\x0a"); -$res->parse("\x0d\x0a16\x0d\x0a"); -$res->parse("Content-ID: 600050\x0d\x0a\x0d\x0a"); -$res->parse("\x0d"); -$res->parse("\x0a6"); -$res->parse("\x0d\x0aabcd\x0d\x0a"); -$res->parse("\x0d\x0a7\x0d\x0a"); -$res->parse("--AAA\x0d\x0a"); -$res->parse("\x0d\x0a1a\x0d\x0a"); -$res->parse("Content-Type: image/jpeg\x0d\x0a"); -$res->parse("\x0d\x0a16\x0d\x0a"); -$res->parse("Content-ID: 600051\x0d\x0a\x0d\x0a"); -$res->parse("\x0d\x0a6\x0d\x0a"); -$res->parse("efgh\x0d\x0a"); -$res->parse("\x0d\x0a7\x0d\x0a"); -$res->parse('--AAA--'); -ok !$res->is_finished, 'response is not finished'; -$res->parse("\x0d\x0a0\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'multipart/parallel; boundary=AAA; charset=utf-8', 'right "Content-Type" value'; -is $res->headers->content_length, 129, 'right "Content-Length" value'; -is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; -is $res->body_size, 129, 'right size'; -ok !$res->content->parts->[0]->is_multipart, 'no multipart content'; -ok !$res->content->parts->[1]->is_multipart, 'no multipart content'; -is $res->content->parts->[0]->asset->slurp, 'abcd', 'right content'; -is $res->content->parts->[0]->headers->content_type, 'image/jpeg', 'right "Content-Type" value'; -is $res->content->parts->[0]->headers->header('Content-ID'), 600050, 'right "Content-ID" value'; -is $res->content->parts->[1]->asset->slurp, 'efgh', 'right content'; -is $res->content->parts->[1]->headers->content_type, 'image/jpeg', 'right "Content-Type" value'; -is $res->content->parts->[1]->headers->header('Content-ID'), 600051, 'right "Content-ID" value'; - -# Parse HTTP 1.1 multipart response with missing boundary -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0d\x0a"); -$res->parse("Content-Length: 420\x0d\x0a"); -$res->parse("Content-Type: multipart/form-data; bo\x0d\x0a\x0d\x0a"); -$res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); -$res->parse("Content-Disposition: form-data; name=\"text1\"\x0d\x0a"); -$res->parse("\x0d\x0ahallo welt test123\n"); -$res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); -$res->parse("Content-Disposition: form-data; name=\"text2\"\x0d\x0a"); -$res->parse("\x0d\x0a\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); -$res->parse('Content-Disposition: form-data; name="upload"; file'); -$res->parse("name=\"hello.pl\"\x0d\x0a\x0d\x0a"); -$res->parse("Content-Type: application/octet-stream\x0d\x0a\x0d\x0a"); -$res->parse("#!/usr/bin/perl\n\n"); -$res->parse("use strict;\n"); -$res->parse("use warnings;\n\n"); -$res->parse("print \"Hello World :)\\n\"\n"); -$res->parse("\x0d\x0a------------0xKhTmLbOuNdArY--"); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'multipart/form-data; bo', 'right "Content-Type" value'; -ok !$res->content->is_multipart, 'no multipart content'; -like $res->content->asset->slurp, qr/hallo welt/, 'right content'; - -# Parse HTTP 1.1 gzip compressed response -my $uncompressed = 'abc' x 1000; -my $compressed = gzip $uncompressed; -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Content-Length: @{[length $compressed]}\x0d\x0a"); -$res->parse("Content-Encoding: GZip\x0d\x0a\x0d\x0a"); -ok $res->content->is_compressed, 'content is compressed'; -is $res->content->progress, 0, 'right progress'; -$res->parse(substr($compressed, 0, 1)); -is $res->content->progress, 1, 'right progress'; -$res->parse(substr($compressed, 1, length($compressed))); -is $res->content->progress, length($compressed), 'right progress'; -ok !$res->content->is_compressed, 'content is not compressed anymore'; -ok $res->is_finished, 'response is finished'; -ok !$res->error, 'no error'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, length($uncompressed), 'right "Content-Length" value'; -is $res->headers->content_encoding, undef, 'no "Content-Encoding" value'; -is $res->body, $uncompressed, 'right content'; - -# Parse HTTP 1.1 chunked gzip compressed response -$uncompressed = 'abc' x 1000; -$compressed = undef; -$compressed = gzip $uncompressed; -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Content-Encoding: gzip\x0d\x0a"); -$res->parse("Transfer-Encoding: chunked\x0d\x0a\x0d\x0a"); -ok $res->content->is_chunked, 'content is chunked'; -ok $res->content->is_compressed, 'content is compressed'; -$res->parse("1\x0d\x0a"); -$res->parse(substr($compressed, 0, 1)); -$res->parse("\x0d\x0a"); -$res->parse(sprintf('%x', length($compressed) - 1)); -$res->parse("\x0d\x0a"); -$res->parse(substr($compressed, 1, length($compressed) - 1)); -$res->parse("\x0d\x0a"); -$res->parse("0\x0d\x0a\x0d\x0a"); -ok !$res->content->is_chunked, 'content is not chunked anymore'; -ok !$res->content->is_compressed, 'content is not compressed anymore'; -ok $res->is_finished, 'response is finished'; -ok !$res->error, 'no error'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, length($uncompressed), 'right "Content-Length" value'; -is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; -is $res->headers->content_encoding, undef, 'no "Content-Encoding" value'; -is $res->body, $uncompressed, 'right content'; - -# Build HTTP 1.1 response start-line with minimal headers -$res = Mojo::Message::Response->new; -$res->code(404); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -$res = Mojo::Message::Response->new->parse($res->to_string); -ok $res->is_finished, 'response is finished'; -is $res->code, '404', 'right status'; -is $res->message, 'Not Found', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; -is $res->headers->content_length, 0, 'right "Content-Length" value'; - -# Build HTTP 1.1 response start-line with minimal headers (strange message) -$res = Mojo::Message::Response->new; -$res->code(404); -$res->message('Looks-0k!@ ;\':" #$%^<>,.\\o/ &*()'); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -$res = Mojo::Message::Response->new->parse($res->to_string); -ok $res->is_finished, 'response is finished'; -is $res->code, '404', 'right status'; -is $res->message, 'Looks-0k!@ ;\':" #$%^<>,.\\o/ &*()', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; -is $res->headers->content_length, 0, 'right "Content-Length" value'; - -# Build HTTP 1.1 response start-line and header -$res = Mojo::Message::Response->new; -$res->code(200); -$res->headers->connection('keep-alive'); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -$res = Mojo::Message::Response->new->parse($res->to_string); -ok $res->is_finished, 'response is finished'; -is $res->code, '200', 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->connection, 'keep-alive', 'right "Connection" value'; -is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; - -# Build full HTTP 1.1 response -$res = Mojo::Message::Response->new; -$res->code(200); -$res->headers->connection('keep-alive'); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -$res->body("Hello World!\n"); -$res = Mojo::Message::Response->new->parse($res->to_string); -ok $res->is_finished, 'response is finished'; -is $res->code, '200', 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->connection, 'keep-alive', 'right "Connection" value'; -is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; -is $res->headers->content_length, '13', 'right "Content-Length" value'; -is $res->body, "Hello World!\n", 'right content'; - -# Build HTTP 1.1 response parts with progress -$res = Mojo::Message::Response->new; -my ($finished, $state, $progress); -$res->on(finish => sub { $finished = shift->is_finished }); -$res->on( - progress => sub { - my ($res, $part, $offset) = @_; - $state = $part; - $progress += $offset; +}; + +subtest "Parse HTTP 1.1 chunked response" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 500 Internal Server Error\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Transfer-Encoding: chunked\x0d\x0a\x0d\x0a"); + $res->parse("4\x0d\x0a"); + $res->parse("abcd\x0d\x0a"); + $res->parse("9\x0d\x0a"); + $res->parse("abcdefghi\x0d\x0a"); + $res->parse("0\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + is $res->code, 500, 'right status'; + is $res->message, 'Internal Server Error', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, 13, 'right "Content-Length" value'; + is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; + is $res->body_size, 13, 'right size'; +}; + +subtest "Parse HTTP 1.1 multipart response" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); + $res->parse("Content-Length: 420\x0d\x0a"); + $res->parse('Content-Type: multipart/form-data; bo'); + $res->parse("undary=----------0xKhTmLbOuNdArY\x0d\x0a\x0d\x0a"); + $res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); + $res->parse("Content-Disposition: form-data; name=\"text1\"\x0d\x0a"); + $res->parse("\x0d\x0ahallo welt test123\n"); + $res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); + $res->parse("Content-Disposition: form-data; name=\"text2\"\x0d\x0a"); + $res->parse("\x0d\x0a\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); + $res->parse('Content-Disposition: form-data; name="upload"; file'); + $res->parse("name=\"hello.pl\"\x0d\x0a\x0d\x0a"); + $res->parse("Content-Type: application/octet-stream\x0d\x0a\x0d\x0a"); + $res->parse("#!/usr/bin/perl\n\n"); + $res->parse("use strict;\n"); + $res->parse("use warnings;\n\n"); + $res->parse("print \"Hello World :)\\n\"\n"); + $res->parse("\x0d\x0a------------0xKhTmLbOuNdArY--"); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'multipart/form-data; boundary=----------0xKhTmLbOuNdArY', + 'right "Content-Type" value'; + ok !$res->content->parts->[0]->is_multipart, 'no multipart content'; + ok !$res->content->parts->[1]->is_multipart, 'no multipart content'; + ok !$res->content->parts->[2]->is_multipart, 'no multipart content'; + is $res->content->parts->[0]->asset->slurp, "hallo welt test123\n", 'right content'; + + my $dir = tempdir; + my $file = $dir->child('multipart.html'); + eval { $res->save_to($file) }; + like $@, qr/^Multipart content cannot be saved to files/, 'right error'; +}; + +subtest "Parse HTTP 1.1 chunked multipart response with leftovers (at once)" => sub { + my $res = Mojo::Message::Response->new; + my $multipart + = "HTTP/1.1 200 OK\x0d\x0a" + . "Transfer-Encoding: chunked\x0d\x0a" + . 'Content-Type: multipart/form-data; bo' + . "undary=----------0xKhTmLbOuNdArY\x0d\x0a\x0d\x0a" + . "19f\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a" + . "Content-Disposition: form-data; name=\"text1\"\x0d\x0a" + . "\x0d\x0ahallo welt test123\n" + . "\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a" + . "Content-Disposition: form-data; name=\"text2\"\x0d\x0a" + . "\x0d\x0a\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a" + . 'Content-Disposition: form-data; name="upload"; file' + . "name=\"hello.pl\"\x0d\x0a" + . "Content-Type: application/octet-stream\x0d\x0a\x0d\x0a" + . "#!/usr/bin/perl\n\n" + . "use strict;\n" + . "use warnings;\n\n" + . "print \"Hello World :)\\n\"\n" + . "\x0d\x0a------------0xKhTmLbOuNdA" + . "r\x0d\x0a3\x0d\x0aY--\x0d\x0a" + . "0\x0d\x0a\x0d\x0a" + . "HTTP/1.0 200 OK\x0d\x0a\x0d\x0a"; + $res->parse($multipart); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'multipart/form-data; boundary=----------0xKhTmLbOuNdArY', + 'right "Content-Type" value'; + is $res->headers->content_length, 418, 'right "Content-Length" value'; + is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; + is $res->body_size, 418, 'right size'; + ok !$res->content->parts->[0]->is_multipart, 'no multipart content'; + ok !$res->content->parts->[1]->is_multipart, 'no multipart content'; + ok !$res->content->parts->[2]->is_multipart, 'no multipart content'; + is $res->content->parts->[0]->asset->slurp, "hallo welt test123\n", 'right content'; + is $res->upload('upload')->filename, 'hello.pl', 'right filename'; + ok !$res->upload('upload')->asset->is_file, 'stored in memory'; + is $res->upload('upload')->asset->size, 69, 'right size'; + is $res->content->parts->[2]->headers->content_type, 'application/octet-stream', 'right "Content-Type" value'; + is $res->content->leftovers, "HTTP/1.0 200 OK\x0d\x0a\x0d\x0a", 'next response in leftovers'; +}; + +subtest "Parse HTTP 1.1 chunked multipart response (in multiple small chunks)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); + $res->parse("Transfer-Encoding: chunked\x0d\x0a"); + $res->parse('Content-Type: multipart/parallel; boundary=AAA; charset=utf-8'); + $res->parse("\x0d\x0a\x0d\x0a"); + $res->parse("7\x0d\x0a"); + $res->parse("--AAA\x0d\x0a"); + $res->parse("\x0d\x0a1a\x0d\x0a"); + $res->parse("Content-Type: image/jpeg\x0d\x0a"); + $res->parse("\x0d\x0a16\x0d\x0a"); + $res->parse("Content-ID: 600050\x0d\x0a\x0d\x0a"); + $res->parse("\x0d"); + $res->parse("\x0a6"); + $res->parse("\x0d\x0aabcd\x0d\x0a"); + $res->parse("\x0d\x0a7\x0d\x0a"); + $res->parse("--AAA\x0d\x0a"); + $res->parse("\x0d\x0a1a\x0d\x0a"); + $res->parse("Content-Type: image/jpeg\x0d\x0a"); + $res->parse("\x0d\x0a16\x0d\x0a"); + $res->parse("Content-ID: 600051\x0d\x0a\x0d\x0a"); + $res->parse("\x0d\x0a6\x0d\x0a"); + $res->parse("efgh\x0d\x0a"); + $res->parse("\x0d\x0a7\x0d\x0a"); + $res->parse('--AAA--'); + ok !$res->is_finished, 'response is not finished'; + + $res->parse("\x0d\x0a0\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'multipart/parallel; boundary=AAA; charset=utf-8', 'right "Content-Type" value'; + is $res->headers->content_length, 129, 'right "Content-Length" value'; + is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; + is $res->body_size, 129, 'right size'; + ok !$res->content->parts->[0]->is_multipart, 'no multipart content'; + ok !$res->content->parts->[1]->is_multipart, 'no multipart content'; + is $res->content->parts->[0]->asset->slurp, 'abcd', 'right content'; + is $res->content->parts->[0]->headers->content_type, 'image/jpeg', 'right "Content-Type" value'; + is $res->content->parts->[0]->headers->header('Content-ID'), 600050, 'right "Content-ID" value'; + is $res->content->parts->[1]->asset->slurp, 'efgh', 'right content'; + is $res->content->parts->[1]->headers->content_type, 'image/jpeg', 'right "Content-Type" value'; + is $res->content->parts->[1]->headers->header('Content-ID'), 600051, 'right "Content-ID" value'; +}; + +subtest "Parse HTTP 1.1 multipart response with missing boundary" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); + $res->parse("Content-Length: 420\x0d\x0a"); + $res->parse("Content-Type: multipart/form-data; bo\x0d\x0a\x0d\x0a"); + $res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); + $res->parse("Content-Disposition: form-data; name=\"text1\"\x0d\x0a"); + $res->parse("\x0d\x0ahallo welt test123\n"); + $res->parse("\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); + $res->parse("Content-Disposition: form-data; name=\"text2\"\x0d\x0a"); + $res->parse("\x0d\x0a\x0d\x0a------------0xKhTmLbOuNdArY\x0d\x0a"); + $res->parse('Content-Disposition: form-data; name="upload"; file'); + $res->parse("name=\"hello.pl\"\x0d\x0a\x0d\x0a"); + $res->parse("Content-Type: application/octet-stream\x0d\x0a\x0d\x0a"); + $res->parse("#!/usr/bin/perl\n\n"); + $res->parse("use strict;\n"); + $res->parse("use warnings;\n\n"); + $res->parse("print \"Hello World :)\\n\"\n"); + $res->parse("\x0d\x0a------------0xKhTmLbOuNdArY--"); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'multipart/form-data; bo', 'right "Content-Type" value'; + ok !$res->content->is_multipart, 'no multipart content'; + like $res->content->asset->slurp, qr/hallo welt/, 'right content'; +}; + +subtest "Parse HTTP 1.1 gzip compressed response" => sub { + my $uncompressed = 'abc' x 1000; + my $compressed = gzip $uncompressed; + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Content-Length: @{[length $compressed]}\x0d\x0a"); + $res->parse("Content-Encoding: GZip\x0d\x0a\x0d\x0a"); + ok $res->content->is_compressed, 'content is compressed'; + is $res->content->progress, 0, 'right progress'; + + $res->parse(substr($compressed, 0, 1)); + is $res->content->progress, 1, 'right progress'; + + $res->parse(substr($compressed, 1, length($compressed))); + is $res->content->progress, length($compressed), 'right progress'; + ok !$res->content->is_compressed, 'content is not compressed anymore'; + ok $res->is_finished, 'response is finished'; + ok !$res->error, 'no error'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, length($uncompressed), 'right "Content-Length" value'; + is $res->headers->content_encoding, undef, 'no "Content-Encoding" value'; + is $res->body, $uncompressed, 'right content'; +}; + +subtest "Parse HTTP 1.1 chunked gzip compressed response" => sub { + my $uncompressed = 'abc' x 1000; + my $compressed = gzip $uncompressed; + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Content-Encoding: gzip\x0d\x0a"); + $res->parse("Transfer-Encoding: chunked\x0d\x0a\x0d\x0a"); + ok $res->content->is_chunked, 'content is chunked'; + ok $res->content->is_compressed, 'content is compressed'; + + $res->parse("1\x0d\x0a"); + $res->parse(substr($compressed, 0, 1)); + $res->parse("\x0d\x0a"); + $res->parse(sprintf('%x', length($compressed) - 1)); + $res->parse("\x0d\x0a"); + $res->parse(substr($compressed, 1, length($compressed) - 1)); + $res->parse("\x0d\x0a"); + $res->parse("0\x0d\x0a\x0d\x0a"); + ok !$res->content->is_chunked, 'content is not chunked anymore'; + ok !$res->content->is_compressed, 'content is not compressed anymore'; + ok $res->is_finished, 'response is finished'; + ok !$res->error, 'no error'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, length($uncompressed), 'right "Content-Length" value'; + is $res->headers->transfer_encoding, undef, 'no "Transfer-Encoding" value'; + is $res->headers->content_encoding, undef, 'no "Content-Encoding" value'; + is $res->body, $uncompressed, 'right content'; +}; + +subtest "Build HTTP 1.1 response start-line with minimal headers" => sub { + my $res = Mojo::Message::Response->new; + $res->code(404); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + $res = Mojo::Message::Response->new->parse($res->to_string); + ok $res->is_finished, 'response is finished'; + is $res->code, '404', 'right status'; + is $res->message, 'Not Found', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; + is $res->headers->content_length, 0, 'right "Content-Length" value'; +}; + +subtest "Build HTTP 1.1 response start-line with minimal headers (strange message)" => sub { + my $res = Mojo::Message::Response->new; + $res->code(404); + $res->message('Looks-0k!@ ;\':" #$%^<>,.\\o/ &*()'); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + $res = Mojo::Message::Response->new->parse($res->to_string); + ok $res->is_finished, 'response is finished'; + is $res->code, '404', 'right status'; + is $res->message, 'Looks-0k!@ ;\':" #$%^<>,.\\o/ &*()', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; + is $res->headers->content_length, 0, 'right "Content-Length" value'; +}; + +subtest "Build HTTP 1.1 response start-line and header" => sub { + my $res = Mojo::Message::Response->new; + $res->code(200); + $res->headers->connection('keep-alive'); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + $res = Mojo::Message::Response->new->parse($res->to_string); + ok $res->is_finished, 'response is finished'; + is $res->code, '200', 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->connection, 'keep-alive', 'right "Connection" value'; + is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; +}; + +subtest "Build full HTTP 1.1 response" => sub { + my $res = Mojo::Message::Response->new; + $res->code(200); + $res->headers->connection('keep-alive'); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + $res->body("Hello World!\n"); + $res = Mojo::Message::Response->new->parse($res->to_string); + ok $res->is_finished, 'response is finished'; + is $res->code, '200', 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->connection, 'keep-alive', 'right "Connection" value'; + is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; + is $res->headers->content_length, '13', 'right "Content-Length" value'; + is $res->body, "Hello World!\n", 'right content'; +}; + +subtest "Build HTTP 1.1 response parts with progress" => sub { + my $res = Mojo::Message::Response->new; + my ($finished, $state, $progress); + $res->on(finish => sub { $finished = shift->is_finished }); + $res->on( + progress => sub { + my ($res, $part, $offset) = @_; + $state = $part; + $progress += $offset; + } + ); + $res->code(200); + $res->headers->connection('keep-alive'); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + $res->body("Hello World!\n"); + ok !$state, 'no state'; + ok !$progress, 'no progress'; + ok !$finished, 'not finished'; + ok $res->build_start_line, 'built start-line'; + is $state, 'start_line', 'made progress on start_line'; + ok $progress, 'made progress'; + + $progress = 0; + ok !$finished, 'not finished'; + ok $res->build_headers, 'built headers'; + is $state, 'headers', 'made progress on headers'; + ok $progress, 'made progress'; + + $progress = 0; + ok !$finished, 'not finished'; + ok $res->build_body, 'built body'; + is $state, 'body', 'made progress on headers'; + ok $progress, 'made progress'; + ok $finished, 'finished'; +}; + +subtest "Build HTTP 1.1 response with dynamic content" => sub { + my $res = Mojo::Message::Response->new; + $res->code(200); + $res->content->write_chunk( + 'Hello ' => sub { + shift->write_chunk(undef, sub { shift->write_chunk('World!')->write_chunk('') }); + } + ); + ok $res->content->is_dynamic, 'dynamic content'; + + $res = Mojo::Message::Response->new->parse($res->to_string); + ok !$res->content->is_dynamic, 'no dynamic content'; + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->content_length, '12', 'right "Content-Length" value'; + is $res->body, 'Hello World!', 'right content'; +}; + +subtest "Build HTTP 1.1 multipart response" => sub { + my $res = Mojo::Message::Response->new; + $res->content(Mojo::Content::MultiPart->new); + $res->code(200); + $res->headers->content_type('multipart/mixed; boundary=7am1X'); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + push @{$res->content->parts}, Mojo::Content::Single->new(asset => Mojo::Asset::File->new); + $res->content->parts->[-1]->asset->add_chunk('Hallo Welt lalalalalala!'); + my $content = Mojo::Content::Single->new; + $content->asset->add_chunk("lala\nfoobar\nperl rocks\n"); + $content->headers->content_type('text/plain'); + push @{$res->content->parts}, $content; + $res = Mojo::Message::Response->new->parse($res->to_string); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; + is $res->headers->content_length, '110', 'right "Content-Length" value'; + is $res->headers->content_type, 'multipart/mixed; boundary=7am1X', 'right "Content-Type" value'; + is $res->content->parts->[0]->asset->slurp, 'Hallo Welt lalalalalala!', 'right content'; + is $res->content->parts->[1]->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->content->parts->[1]->asset->slurp, "lala\nfoobar\nperl rocks\n", 'right content'; +}; + +subtest "Parse response with cookie" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.0 200 OK\x0d\x0a"); + $res->parse("Content-Type: text/plain\x0d\x0a"); + $res->parse("Content-Length: 27\x0d\x0a"); + $res->parse("Set-Cookie: foo=bar; path=/test\x0d\x0a\x0d\x0a"); + $res->parse("Hello World!\n1234\nlalalala\n"); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.0', 'right version'; + is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; + is $res->headers->content_length, 27, 'right "Content-Length" value'; + is $res->headers->set_cookie, 'foo=bar; path=/test', 'right "Set-Cookie" value'; + my $cookies = $res->cookies; + is $cookies->[0]->name, 'foo', 'right name'; + is $cookies->[0]->value, 'bar', 'right value'; + is $cookies->[0]->path, '/test', 'right path'; + is $res->cookie('foo')->value, 'bar', 'right value'; + is $res->cookie('foo')->path, '/test', 'right path'; +}; + +subtest "Parse WebSocket handshake response" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 101 Switching Protocols\x0d\x0a"); + $res->parse("Upgrade: websocket\x0d\x0a"); + $res->parse("Connection: Upgrade\x0d\x0a"); + $res->parse("Sec-WebSocket-Accept: abcdef=\x0d\x0a"); + $res->parse("Sec-WebSocket-Protocol: sample\x0d\x0a\x0d\x0a"); + ok $res->is_finished, 'response is finished'; + ok $res->is_empty, 'response is empty'; + ok $res->content->skip_body, 'body has been skipped'; + is $res->code, 101, 'right status'; + is $res->message, 'Switching Protocols', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->upgrade, 'websocket', 'right "Upgrade" value'; + is $res->headers->connection, 'Upgrade', 'right "Connection" value'; + is $res->headers->sec_websocket_accept, 'abcdef=', 'right "Sec-WebSocket-Accept" value'; + is $res->headers->sec_websocket_protocol, 'sample', 'right "Sec-WebSocket-Protocol" value'; + is $res->body, '', 'no content'; + ok !defined $res->headers->content_length, '"Content-Length" does not exist'; +}; + +subtest "Parse WebSocket handshake response (with frame)" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 101 Switching Protocols\x0d\x0a"); + $res->parse("Upgrade: websocket\x0d\x0a"); + $res->parse("Connection: Upgrade\x0d\x0a"); + $res->parse("Sec-WebSocket-Accept: abcdef=\x0d\x0a"); + $res->parse("Sec-WebSocket-Protocol: sample\x0d\x0a"); + $res->parse("\x0d\x0a\x81\x08\x77\x68\x61\x74\x65\x76\x65\x72"); + ok $res->is_finished, 'response is finished'; + ok $res->is_empty, 'response is empty'; + ok $res->content->skip_body, 'body has been skipped'; + is $res->code, 101, 'right status'; + is $res->message, 'Switching Protocols', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->upgrade, 'websocket', 'right "Upgrade" value'; + is $res->headers->connection, 'Upgrade', 'right "Connection" value'; + is $res->headers->sec_websocket_accept, 'abcdef=', 'right "Sec-WebSocket-Accept" value'; + is $res->headers->sec_websocket_protocol, 'sample', 'right "Sec-WebSocket-Protocol" value'; + is $res->body, '', 'no content'; + is $res->content->leftovers, "\x81\x08\x77\x68\x61\x74\x65\x76\x65\x72", 'frame in leftovers'; + ok !defined $res->headers->content_length, '"Content-Length" does not exist'; +}; + +subtest "Build WebSocket handshake response" => sub { + my $res = Mojo::Message::Response->new; + $res->code(101); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + $res->headers->upgrade('websocket'); + $res->headers->connection('Upgrade'); + $res->headers->sec_websocket_accept('abcdef='); + $res->headers->sec_websocket_protocol('sample'); + $res = Mojo::Message::Response->new->parse($res->to_string); + ok $res->is_finished, 'response is finished'; + is $res->code, '101', 'right status'; + is $res->message, 'Switching Protocols', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->headers->connection, 'Upgrade', 'right "Connection" value'; + is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; + ok !defined $res->headers->content_length, '"Content-Length" does not exist'; + is $res->headers->upgrade, 'websocket', 'right "Upgrade" value'; + is $res->headers->sec_websocket_accept, 'abcdef=', 'right "Sec-WebSocket-Accept" value'; + is $res->headers->sec_websocket_protocol, 'sample', 'right "Sec-WebSocket-Protocol" value'; + is $res->body, '', 'no content'; + ok !defined $res->headers->content_length, '"Content-Length" does not exist'; +}; + +subtest "Build and parse HTTP 1.1 response with 3 cookies" => sub { + my $res = Mojo::Message::Response->new; + $res->code(404); + $res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); + $res->cookies({name => 'foo', value => 'bar', path => '/foobar'}, + {name => 'bar', value => 'baz', path => '/test/23'}); + $res->cookies({name => 'baz', value => 'yada', path => '/foobar'}); + ok !!$res->to_string, 'message built'; + + my $res2 = Mojo::Message::Response->new; + $res2->parse($res->to_string); + ok $res2->is_finished, 'response is finished'; + is $res2->code, 404, 'right status'; + is $res2->version, '1.1', 'right version'; + is $res2->headers->content_length, 0, 'right "Content-Length" value'; + ok defined $res2->cookie('foo'), 'cookie "foo" exists'; + ok defined $res2->cookie('bar'), 'cookie "bar" exists'; + ok defined $res2->cookie('baz'), 'cookie "baz" exists'; + ok !defined $res2->cookie('yada'), 'cookie "yada" does not exist'; + is $res2->cookie('foo')->path, '/foobar', 'right path'; + is $res2->cookie('foo')->value, 'bar', 'right value'; + is $res2->cookie('bar')->path, '/test/23', 'right path'; + is $res2->cookie('bar')->value, 'baz', 'right value'; + is $res2->cookie('baz')->path, '/foobar', 'right path'; + is $res2->cookie('baz')->value, 'yada', 'right value'; +}; + +subtest "Build chunked response body" => sub { + my $res = Mojo::Message::Response->new; + $res->code(200); + my $invocant; + $res->content->write_chunk('hello!' => sub { $invocant = shift }); + $res->content->write_chunk('hello world!')->write_chunk(''); + ok $res->content->is_chunked, 'chunked content'; + ok $res->content->is_dynamic, 'dynamic content'; + is $res->build_body, "6\x0d\x0ahello!\x0d\x0ac\x0d\x0ahello world!\x0d\x0a0\x0d\x0a\x0d\x0a", 'right format'; + isa_ok $invocant, 'Mojo::Content::Single', 'right invocant'; +}; + +subtest "Build dynamic response body" => sub { + my $res = Mojo::Message::Response->new; + $res->code(200); + my $invocant = undef; + $res->content->write('hello!' => sub { $invocant = shift }); + $res->content->write('hello world!')->write(''); + ok !$res->content->is_chunked, 'no chunked content'; + ok $res->content->is_dynamic, 'dynamic content'; + is $res->build_body, "hello!hello world!", 'right format'; + isa_ok $invocant, 'Mojo::Content::Single', 'right invocant'; +}; + +subtest "Build response with callback (make sure it's called)" => sub { + my $res = Mojo::Message::Response->new; + $res->code(200); + $res->headers->content_length(10); + $res->content->write('lala' => sub { die "Body callback was called properly\n" }); + $res->get_body_chunk(0); + eval { $res->get_body_chunk(3) }; + is $@, "Body callback was called properly\n", 'right error'; +}; + +subtest "Build response with callback (consistency calls)" => sub { + my $res = Mojo::Message::Response->new; + my $body = 'I is here'; + $res->headers->content_length(length($body)); + my $cb = sub { shift->write(substr($body, pop, 1), __SUB__) }; + $res->content->write('' => $cb); + my $full = ''; + my $count = 0; + my $offset = 0; + + while (1) { + my $chunk = $res->get_body_chunk($offset); + last unless $chunk; + $full .= $chunk; + $offset = length($full); + $count++; } -); -$res->code(200); -$res->headers->connection('keep-alive'); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -$res->body("Hello World!\n"); -ok !$state, 'no state'; -ok !$progress, 'no progress'; -ok !$finished, 'not finished'; -ok $res->build_start_line, 'built start-line'; -is $state, 'start_line', 'made progress on start_line'; -ok $progress, 'made progress'; -$progress = 0; -ok !$finished, 'not finished'; -ok $res->build_headers, 'built headers'; -is $state, 'headers', 'made progress on headers'; -ok $progress, 'made progress'; -$progress = 0; -ok !$finished, 'not finished'; -ok $res->build_body, 'built body'; -is $state, 'body', 'made progress on headers'; -ok $progress, 'made progress'; -ok $finished, 'finished'; - -# Build HTTP 1.1 response with dynamic content -$res = Mojo::Message::Response->new; -$res->code(200); -$res->content->write_chunk( - 'Hello ' => sub { - shift->write_chunk(undef, sub { shift->write_chunk('World!')->write_chunk('') }); + $res->fix_headers; + is $res->headers->connection, undef, 'no "Connection" value'; + ok $res->content->is_dynamic, 'dynamic content'; + is $count, length($body), 'right length'; + is $full, $body, 'right content'; +}; + +subtest "Build response with callback (no Content-Length header)" => sub { + my $res = Mojo::Message::Response->new; + my $body = 'I is here'; + my $cb; + $cb = sub { shift->write(substr($body, pop, 1), $cb) }; + $res->content->write('' => $cb); + $res->fix_headers; + my $full = ''; + my $count = 0; + my $offset = 0; + + while (1) { + my $chunk = $res->get_body_chunk($offset); + last unless $chunk; + $full .= $chunk; + $offset = length($full); + $count++; } -); -ok $res->content->is_dynamic, 'dynamic content'; -$res = Mojo::Message::Response->new->parse($res->to_string); -ok !$res->content->is_dynamic, 'no dynamic content'; -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->content_length, '12', 'right "Content-Length" value'; -is $res->body, 'Hello World!', 'right content'; - -# Build HTTP 1.1 multipart response -$res = Mojo::Message::Response->new; -$res->content(Mojo::Content::MultiPart->new); -$res->code(200); -$res->headers->content_type('multipart/mixed; boundary=7am1X'); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -push @{$res->content->parts}, Mojo::Content::Single->new(asset => Mojo::Asset::File->new); -$res->content->parts->[-1]->asset->add_chunk('Hallo Welt lalalalalala!'); -my $content = Mojo::Content::Single->new; -$content->asset->add_chunk("lala\nfoobar\nperl rocks\n"); -$content->headers->content_type('text/plain'); -push @{$res->content->parts}, $content; -$res = Mojo::Message::Response->new->parse($res->to_string); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; -is $res->headers->content_length, '110', 'right "Content-Length" value'; -is $res->headers->content_type, 'multipart/mixed; boundary=7am1X', 'right "Content-Type" value'; -is $res->content->parts->[0]->asset->slurp, 'Hallo Welt lalalalalala!', 'right content'; -is $res->content->parts->[1]->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->content->parts->[1]->asset->slurp, "lala\nfoobar\nperl rocks\n", 'right content'; - -# Parse response with cookie -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.0 200 OK\x0d\x0a"); -$res->parse("Content-Type: text/plain\x0d\x0a"); -$res->parse("Content-Length: 27\x0d\x0a"); -$res->parse("Set-Cookie: foo=bar; path=/test\x0d\x0a\x0d\x0a"); -$res->parse("Hello World!\n1234\nlalalala\n"); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.0', 'right version'; -is $res->headers->content_type, 'text/plain', 'right "Content-Type" value'; -is $res->headers->content_length, 27, 'right "Content-Length" value'; -is $res->headers->set_cookie, 'foo=bar; path=/test', 'right "Set-Cookie" value'; -my $cookies = $res->cookies; -is $cookies->[0]->name, 'foo', 'right name'; -is $cookies->[0]->value, 'bar', 'right value'; -is $cookies->[0]->path, '/test', 'right path'; -is $res->cookie('foo')->value, 'bar', 'right value'; -is $res->cookie('foo')->path, '/test', 'right path'; - -# Parse WebSocket handshake response -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 101 Switching Protocols\x0d\x0a"); -$res->parse("Upgrade: websocket\x0d\x0a"); -$res->parse("Connection: Upgrade\x0d\x0a"); -$res->parse("Sec-WebSocket-Accept: abcdef=\x0d\x0a"); -$res->parse("Sec-WebSocket-Protocol: sample\x0d\x0a\x0d\x0a"); -ok $res->is_finished, 'response is finished'; -ok $res->is_empty, 'response is empty'; -ok $res->content->skip_body, 'body has been skipped'; -is $res->code, 101, 'right status'; -is $res->message, 'Switching Protocols', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->upgrade, 'websocket', 'right "Upgrade" value'; -is $res->headers->connection, 'Upgrade', 'right "Connection" value'; -is $res->headers->sec_websocket_accept, 'abcdef=', 'right "Sec-WebSocket-Accept" value'; -is $res->headers->sec_websocket_protocol, 'sample', 'right "Sec-WebSocket-Protocol" value'; -is $res->body, '', 'no content'; -ok !defined $res->headers->content_length, '"Content-Length" does not exist'; - -# Parse WebSocket handshake response (with frame) -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 101 Switching Protocols\x0d\x0a"); -$res->parse("Upgrade: websocket\x0d\x0a"); -$res->parse("Connection: Upgrade\x0d\x0a"); -$res->parse("Sec-WebSocket-Accept: abcdef=\x0d\x0a"); -$res->parse("Sec-WebSocket-Protocol: sample\x0d\x0a"); -$res->parse("\x0d\x0a\x81\x08\x77\x68\x61\x74\x65\x76\x65\x72"); -ok $res->is_finished, 'response is finished'; -ok $res->is_empty, 'response is empty'; -ok $res->content->skip_body, 'body has been skipped'; -is $res->code, 101, 'right status'; -is $res->message, 'Switching Protocols', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->upgrade, 'websocket', 'right "Upgrade" value'; -is $res->headers->connection, 'Upgrade', 'right "Connection" value'; -is $res->headers->sec_websocket_accept, 'abcdef=', 'right "Sec-WebSocket-Accept" value'; -is $res->headers->sec_websocket_protocol, 'sample', 'right "Sec-WebSocket-Protocol" value'; -is $res->body, '', 'no content'; -is $res->content->leftovers, "\x81\x08\x77\x68\x61\x74\x65\x76\x65\x72", 'frame in leftovers'; -ok !defined $res->headers->content_length, '"Content-Length" does not exist'; - -# Build WebSocket handshake response -$res = Mojo::Message::Response->new; -$res->code(101); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -$res->headers->upgrade('websocket'); -$res->headers->connection('Upgrade'); -$res->headers->sec_websocket_accept('abcdef='); -$res->headers->sec_websocket_protocol('sample'); -$res = Mojo::Message::Response->new->parse($res->to_string); -ok $res->is_finished, 'response is finished'; -is $res->code, '101', 'right status'; -is $res->message, 'Switching Protocols', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->headers->connection, 'Upgrade', 'right "Connection" value'; -is $res->headers->date, 'Sun, 17 Aug 2008 16:27:35 GMT', 'right "Date" value'; -ok !defined $res->headers->content_length, '"Content-Length" does not exist'; -is $res->headers->upgrade, 'websocket', 'right "Upgrade" value'; -is $res->headers->sec_websocket_accept, 'abcdef=', 'right "Sec-WebSocket-Accept" value'; -is $res->headers->sec_websocket_protocol, 'sample', 'right "Sec-WebSocket-Protocol" value'; -is $res->body, '', 'no content'; -ok !defined $res->headers->content_length, '"Content-Length" does not exist'; - -# Build and parse HTTP 1.1 response with 3 cookies -$res = Mojo::Message::Response->new; -$res->code(404); -$res->headers->date('Sun, 17 Aug 2008 16:27:35 GMT'); -$res->cookies({name => 'foo', value => 'bar', path => '/foobar'}, {name => 'bar', value => 'baz', path => '/test/23'}); -$res->cookies({name => 'baz', value => 'yada', path => '/foobar'}); -ok !!$res->to_string, 'message built'; -my $res2 = Mojo::Message::Response->new; -$res2->parse($res->to_string); -ok $res2->is_finished, 'response is finished'; -is $res2->code, 404, 'right status'; -is $res2->version, '1.1', 'right version'; -is $res2->headers->content_length, 0, 'right "Content-Length" value'; -ok defined $res2->cookie('foo'), 'cookie "foo" exists'; -ok defined $res2->cookie('bar'), 'cookie "bar" exists'; -ok defined $res2->cookie('baz'), 'cookie "baz" exists'; -ok !defined $res2->cookie('yada'), 'cookie "yada" does not exist'; -is $res2->cookie('foo')->path, '/foobar', 'right path'; -is $res2->cookie('foo')->value, 'bar', 'right value'; -is $res2->cookie('bar')->path, '/test/23', 'right path'; -is $res2->cookie('bar')->value, 'baz', 'right value'; -is $res2->cookie('baz')->path, '/foobar', 'right path'; -is $res2->cookie('baz')->value, 'yada', 'right value'; - -# Build chunked response body -$res = Mojo::Message::Response->new; -$res->code(200); -my $invocant; -$res->content->write_chunk('hello!' => sub { $invocant = shift }); -$res->content->write_chunk('hello world!')->write_chunk(''); -ok $res->content->is_chunked, 'chunked content'; -ok $res->content->is_dynamic, 'dynamic content'; -is $res->build_body, "6\x0d\x0ahello!\x0d\x0ac\x0d\x0ahello world!\x0d\x0a0\x0d\x0a\x0d\x0a", 'right format'; -isa_ok $invocant, 'Mojo::Content::Single', 'right invocant'; - -# Build dynamic response body -$res = Mojo::Message::Response->new; -$res->code(200); -$invocant = undef; -$res->content->write('hello!' => sub { $invocant = shift }); -$res->content->write('hello world!')->write(''); -ok !$res->content->is_chunked, 'no chunked content'; -ok $res->content->is_dynamic, 'dynamic content'; -is $res->build_body, "hello!hello world!", 'right format'; -isa_ok $invocant, 'Mojo::Content::Single', 'right invocant'; - -# Build response with callback (make sure it's called) -$res = Mojo::Message::Response->new; -$res->code(200); -$res->headers->content_length(10); -$res->content->write('lala' => sub { die "Body callback was called properly\n" }); -$res->get_body_chunk(0); -eval { $res->get_body_chunk(3) }; -is $@, "Body callback was called properly\n", 'right error'; - -# Build response with callback (consistency calls) -$res = Mojo::Message::Response->new; -my $body = 'I is here'; -$res->headers->content_length(length($body)); -my $cb = sub { shift->write(substr($body, pop, 1), __SUB__) }; -$res->content->write('' => $cb); -my $full = ''; -my $count = 0; -my $offset = 0; - -while (1) { - my $chunk = $res->get_body_chunk($offset); - last unless $chunk; - $full .= $chunk; - $offset = length($full); - $count++; -} -$res->fix_headers; -is $res->headers->connection, undef, 'no "Connection" value'; -ok $res->content->is_dynamic, 'dynamic content'; -is $count, length($body), 'right length'; -is $full, $body, 'right content'; - -# Build response with callback (no Content-Length header) -$res = Mojo::Message::Response->new; -$body = 'I is here'; -$cb = sub { shift->write(substr($body, pop, 1), $cb) }; -$res->content->write('' => $cb); -$res->fix_headers; -$full = ''; -$count = 0; -$offset = 0; - -while (1) { - my $chunk = $res->get_body_chunk($offset); - last unless $chunk; - $full .= $chunk; - $offset = length($full); - $count++; -} -is $res->headers->connection, 'close', 'right "Connection" value'; -ok $res->content->is_dynamic, 'dynamic content'; -is $count, length($body), 'right length'; -is $full, $body, 'right content'; - -# Body -$res = Mojo::Message::Response->new; -$res->body('hi there!'); -ok !$res->content->asset->is_file, 'stored in memory'; -ok !$res->content->asset->auto_upgrade, 'no upgrade'; -is $res->body, 'hi there!', 'right content'; -$res->body(''); -is $res->body, '', 'no content'; -$res->body('hi there!'); -is $res->body, 'hi there!', 'right content'; -$res->body(0); -is $res->body, 0, 'right content'; -is $res->body('hello!')->body, 'hello!', 'right content'; -$res->content(Mojo::Content::MultiPart->new); -$res->body('hi!'); -is $res->body, 'hi!', 'right content'; - -# Text -$res = Mojo::Message::Response->new; -my $snowman = encode 'UTF-8', '☃'; -is $res->body($snowman)->text, '☃', 'right content'; -is $res->body($snowman)->dom->text, '☃', 'right text'; -$res = Mojo::Message::Response->new; -my $yatta = encode 'shift_jis', 'やった'; -is $res->body($yatta)->text, $yatta, 'right content'; -$res->headers->content_type('text/plain;charset=shift_jis'); -is $res->body($yatta)->text, 'やった', 'right content'; -is $res->body($yatta)->dom->text, 'やった', 'right text'; - -# Body exceeding memory limit (no upgrade) -{ - local $ENV{MOJO_MAX_MEMORY_SIZE} = 8; + is $res->headers->connection, 'close', 'right "Connection" value'; + ok $res->content->is_dynamic, 'dynamic content'; + is $count, length($body), 'right length'; + is $full, $body, 'right content'; +}; + +subtest "Body" => sub { + my $res = Mojo::Message::Response->new; + $res->body('hi there!'); + ok !$res->content->asset->is_file, 'stored in memory'; + ok !$res->content->asset->auto_upgrade, 'no upgrade'; + is $res->body, 'hi there!', 'right content'; + + $res->body(''); + is $res->body, '', 'no content'; + + $res->body('hi there!'); + is $res->body, 'hi there!', 'right content'; + + $res->body(0); + is $res->body, 0, 'right content'; + is $res->body('hello!')->body, 'hello!', 'right content'; + + $res->content(Mojo::Content::MultiPart->new); + $res->body('hi!'); + is $res->body, 'hi!', 'right content'; +}; + +subtest "Text" => sub { + my $res = Mojo::Message::Response->new; + my $snowman = encode 'UTF-8', '☃'; + is $res->body($snowman)->text, '☃', 'right content'; + is $res->body($snowman)->dom->text, '☃', 'right text'; + $res = Mojo::Message::Response->new; + my $yatta = encode 'shift_jis', 'やった'; + is $res->body($yatta)->text, $yatta, 'right content'; + + $res->headers->content_type('text/plain;charset=shift_jis'); + is $res->body($yatta)->text, 'やった', 'right content'; + is $res->body($yatta)->dom->text, 'やった', 'right text'; +}; + +subtest "Body exceeding memory limit (no upgrade)" => sub { + local $ENV{MOJO_MAX_MEMORY_SIZE} = 8; + my $res = Mojo::Message::Response->new; $res->body('hi there!'); is $res->body, 'hi there!', 'right content'; is $res->content->asset->max_memory_size, 8, 'right size'; is $res->content->asset->size, 9, 'right size'; ok !$res->content->asset->is_file, 'stored in memory'; -} - -# Parse response and extract JSON data -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0a"); -$res->parse("Content-Type: application/json\x0a"); -$res->parse("Content-Length: 27\x0a\x0a"); -$res->parse(encode_json({foo => 'bar', baz => [1, 2, 3]})); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is_deeply $res->json, {foo => 'bar', baz => [1, 2, 3]}, 'right JSON data'; -is $res->json('/foo'), 'bar', 'right result'; -is $res->json('/baz/1'), 2, 'right result'; -is_deeply $res->json('/baz'), [1, 2, 3], 'right result'; -$res->json->{baz}[1] = 4; -is_deeply $res->json('/baz'), [1, 4, 3], 'right result'; - -# Parse response and extract HTML -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.1 200 OK\x0a"); -$res->parse("Content-Type: text/html\x0a"); -$res->parse("Content-Length: 51\x0a\x0a"); -$res->parse('

foobarbaz

'); -ok $res->is_finished, 'response is finished'; -is $res->code, 200, 'right status'; -is $res->message, 'OK', 'right message'; -is $res->version, '1.1', 'right version'; -is $res->dom->at('p')->text, 'foo', 'right value'; -is $res->dom->at('p > a')->text, 'bar', 'right value'; -is $res->dom('p')->first->text, 'foo', 'right value'; -is_deeply $res->dom('p > a')->map('text')->to_array, [qw(bar baz)], 'right values'; -my @text = $res->dom('a')->map(content => 'yada')->first->root->find('p > a')->map('text')->each; -is_deeply \@text, [qw(yada yada)], 'right values'; -is_deeply $res->dom('p > a')->map('text')->to_array, [qw(yada yada)], 'right values'; -@text = $res->dom->find('a')->map(content => 'test')->first->root->find('p > a')->map('text')->each; -is_deeply \@text, [qw(test test)], 'right values'; -is_deeply $res->dom->find('p > a')->map('text')->to_array, [qw(test test)], 'right values'; -$file = $dir->child('single.html'); -is $res->save_to($file)->body, '

foobarbaz

', 'right content'; -is $file->slurp, '

foobarbaz

', 'right content'; - -# Build DOM from response with charset -$res = Mojo::Message::Response->new; -$res->parse("HTTP/1.0 200 OK\x0a"); -$res->parse("Content-Type: application/atom+xml; charset=UTF-8; type=feed\x0a"); -$res->parse("\x0a"); -$res->body('

foo barbaz

'); -ok !$res->is_finished, 'response is not finished'; -is $res->headers->content_type, 'application/atom+xml; charset=UTF-8; type=feed', 'right "Content-Type" value'; -ok $res->dom, 'dom built'; -$count = 0; -$res->dom('a')->each(sub { $count++ }); -is $count, 2, 'all anchors found'; +}; + +subtest "Parse response and extract JSON data" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0a"); + $res->parse("Content-Type: application/json\x0a"); + $res->parse("Content-Length: 27\x0a\x0a"); + $res->parse(encode_json({foo => 'bar', baz => [1, 2, 3]})); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is_deeply $res->json, {foo => 'bar', baz => [1, 2, 3]}, 'right JSON data'; + is $res->json('/foo'), 'bar', 'right result'; + is $res->json('/baz/1'), 2, 'right result'; + is_deeply $res->json('/baz'), [1, 2, 3], 'right result'; + + $res->json->{baz}[1] = 4; + is_deeply $res->json('/baz'), [1, 4, 3], 'right result'; +}; + +subtest "Parse response and extract HTML" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.1 200 OK\x0a"); + $res->parse("Content-Type: text/html\x0a"); + $res->parse("Content-Length: 51\x0a\x0a"); + $res->parse('

foobarbaz

'); + ok $res->is_finished, 'response is finished'; + is $res->code, 200, 'right status'; + is $res->message, 'OK', 'right message'; + is $res->version, '1.1', 'right version'; + is $res->dom->at('p')->text, 'foo', 'right value'; + is $res->dom->at('p > a')->text, 'bar', 'right value'; + is $res->dom('p')->first->text, 'foo', 'right value'; + is_deeply $res->dom('p > a')->map('text')->to_array, [qw(bar baz)], 'right values'; + + my @text = $res->dom('a')->map(content => 'yada')->first->root->find('p > a')->map('text')->each; + is_deeply \@text, [qw(yada yada)], 'right values'; + is_deeply $res->dom('p > a')->map('text')->to_array, [qw(yada yada)], 'right values'; + + @text = $res->dom->find('a')->map(content => 'test')->first->root->find('p > a')->map('text')->each; + is_deeply \@text, [qw(test test)], 'right values'; + is_deeply $res->dom->find('p > a')->map('text')->to_array, [qw(test test)], 'right values'; + + my $dir = tempdir; + my $file = $dir->child('single.html'); + is $res->save_to($file)->body, '

foobarbaz

', 'right content'; + is $file->slurp, '

foobarbaz

', 'right content'; +}; + +subtest "Build DOM from response with charset" => sub { + my $res = Mojo::Message::Response->new; + $res->parse("HTTP/1.0 200 OK\x0a"); + $res->parse("Content-Type: application/atom+xml; charset=UTF-8; type=feed\x0a"); + $res->parse("\x0a"); + $res->body('

foo barbaz

'); + ok !$res->is_finished, 'response is not finished'; + is $res->headers->content_type, 'application/atom+xml; charset=UTF-8; type=feed', 'right "Content-Type" value'; + ok $res->dom, 'dom built'; + + my $count = 0; + $res->dom('a')->each(sub { $count++ }); + is $count, 2, 'all anchors found'; +}; done_testing(); From f91234c82d87279d5bc2121411903ed7142b6f66 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Sat, 31 Oct 2020 21:09:14 -0700 Subject: [PATCH 4/6] Convert roles.t to use subtests --- t/mojo/roles.t | 150 +++++++++++++++++++++++++++---------------------- 1 file changed, 83 insertions(+), 67 deletions(-) diff --git a/t/mojo/roles.t b/t/mojo/roles.t index d81266fceb..1542025d17 100644 --- a/t/mojo/roles.t +++ b/t/mojo/roles.t @@ -52,72 +52,88 @@ use Mojo::Collection; use Mojo::DOM; use Mojo::File; -# Plain class -my $obj = Mojo::RoleTest->new(name => 'Ted'); -is $obj->name, 'Ted', 'attribute'; -is $obj->hello, 'hello Ted', 'method'; - -# Empty roles -my $fred = Mojo::RoleTest->with_roles()->new(name => 'Fred'); -is $fred->name, 'Fred', 'attribute'; -is $fred->hello, 'hello Fred', 'method'; - -# Empty object roles -my $obj_empty = $obj->with_roles(); -is $obj_empty->name, 'Ted', 'attribute'; -is $obj_empty->hello, 'hello Ted', 'method'; - -# Single role -my $obj2 = Mojo::RoleTest->with_roles('Mojo::RoleTest::Role::LOUD')->new; -is $obj2->hello, 'HEY! BOB!!!', 'role method'; -is $obj2->yell, 'HEY!', 'another role method'; - -# Single role (shorthand) -my $obj4 = Mojo::RoleTest->with_roles('+LOUD')->new; -is $obj4->hello, 'HEY! BOB!!!', 'role method'; -is $obj4->yell, 'HEY!', 'another role method'; - -# Multiple roles -my $obj3 = Mojo::RoleTest->with_roles('Mojo::RoleTest::Role::quiet', 'Mojo::RoleTest::Role::LOUD')->new(name => 'Joel'); -is $obj3->name, 'Joel', 'base attribute'; -is $obj3->whisper, 'psst, joel', 'method from first role'; -$obj3->prefix('psssst, '); -is $obj3->whisper, 'psssst, joel', 'method from first role'; -is $obj3->hello, 'HEY! JOEL!!!', 'method from second role'; - -# Multiple roles (shorthand) -my $obj5 = Mojo::RoleTest->with_roles('+quiet', '+LOUD')->new(name => 'Joel'); -is $obj5->name, 'Joel', 'base attribute'; -is $obj5->whisper, 'psst, joel', 'method from first role'; -is $obj5->hello, 'HEY! JOEL!!!', 'method from second role'; - -# Multiple roles (mixed) -my $obj6 = Mojo::RoleTest->with_roles('Mojo::RoleTest::Role::quiet', '+LOUD')->new(name => 'Joel'); -is $obj6->name, 'Joel', 'base attribute'; -is $obj6->whisper, 'psst, joel', 'method from first role'; -is $obj6->hello, 'HEY! JOEL!!!', 'method from second role'; - -# Multiple object roles (mixed) -my $obj7 = Mojo::RoleTest->new(name => 'Joel')->with_roles('Mojo::RoleTest::Role::quiet', '+LOUD'); -is $obj7->name, 'Joel', 'base attribute'; -is $obj7->whisper, 'psst, joel', 'method from first role'; -is $obj7->hello, 'HEY! JOEL!!!', 'method from second role'; - -# Multiple Mojo::Base roles -my $obj8 = Mojo::RoleTest->with_roles('+quiet', 'Mojo::RoleTest::Hello')->new(name => 'Joel'); -is $obj8->name, 'Joel', 'base attribute'; -is $obj8->whisper, 'psst, joel', 'method from first role'; -is $obj8->hello, 'hello mojo!', 'method from second role'; - -# Classes that are not subclasses of Mojo::Base -my $stream = Mojo::ByteStream->with_roles('Mojo::RoleTest::Hello')->new; -is $stream->hello, 'hello mojo!', 'right result'; -my $c = Mojo::Collection->with_roles('Mojo::RoleTest::Hello')->new; -is $c->hello, 'hello mojo!', 'right result'; -my $dom = Mojo::DOM->with_roles('Mojo::RoleTest::Hello')->new; -is $dom->hello, 'hello mojo!', 'right result'; -my $file = Mojo::File->with_roles('Mojo::RoleTest::Hello')->new; -is $file->hello, 'hello mojo!', 'right result'; +my $obj; +subtest "Plain class" => sub { + $obj = Mojo::RoleTest->new(name => 'Ted'); + is $obj->name, 'Ted', 'attribute'; + is $obj->hello, 'hello Ted', 'method'; +}; + +subtest "Empty roles" => sub { + my $fred = Mojo::RoleTest->with_roles()->new(name => 'Fred'); + is $fred->name, 'Fred', 'attribute'; + is $fred->hello, 'hello Fred', 'method'; +}; + +subtest "Empty object roles" => sub { + my $obj_empty = $obj->with_roles(); + is $obj_empty->name, 'Ted', 'attribute'; + is $obj_empty->hello, 'hello Ted', 'method'; +}; + +subtest "Single role" => sub { + my $obj2 = Mojo::RoleTest->with_roles('Mojo::RoleTest::Role::LOUD')->new; + is $obj2->hello, 'HEY! BOB!!!', 'role method'; + is $obj2->yell, 'HEY!', 'another role method'; +}; + +subtest "Single role (shorthand)" => sub { + my $obj4 = Mojo::RoleTest->with_roles('+LOUD')->new; + is $obj4->hello, 'HEY! BOB!!!', 'role method'; + is $obj4->yell, 'HEY!', 'another role method'; +}; + +subtest "Multiple roles" => sub { + my $obj3 + = Mojo::RoleTest->with_roles('Mojo::RoleTest::Role::quiet', 'Mojo::RoleTest::Role::LOUD')->new(name => 'Joel'); + is $obj3->name, 'Joel', 'base attribute'; + is $obj3->whisper, 'psst, joel', 'method from first role'; + + $obj3->prefix('psssst, '); + is $obj3->whisper, 'psssst, joel', 'method from first role'; + is $obj3->hello, 'HEY! JOEL!!!', 'method from second role'; +}; + +subtest "Multiple roles (shorthand)" => sub { + my $obj5 = Mojo::RoleTest->with_roles('+quiet', '+LOUD')->new(name => 'Joel'); + is $obj5->name, 'Joel', 'base attribute'; + is $obj5->whisper, 'psst, joel', 'method from first role'; + is $obj5->hello, 'HEY! JOEL!!!', 'method from second role'; +}; + +subtest "Multiple roles (mixed)" => sub { + my $obj6 = Mojo::RoleTest->with_roles('Mojo::RoleTest::Role::quiet', '+LOUD')->new(name => 'Joel'); + is $obj6->name, 'Joel', 'base attribute'; + is $obj6->whisper, 'psst, joel', 'method from first role'; + is $obj6->hello, 'HEY! JOEL!!!', 'method from second role'; +}; + +subtest "Multiple object roles (mixed)" => sub { + my $obj7 = Mojo::RoleTest->new(name => 'Joel')->with_roles('Mojo::RoleTest::Role::quiet', '+LOUD'); + is $obj7->name, 'Joel', 'base attribute'; + is $obj7->whisper, 'psst, joel', 'method from first role'; + is $obj7->hello, 'HEY! JOEL!!!', 'method from second role'; +}; + +subtest "Multiple Mojo::Base roles" => sub { + my $obj8 = Mojo::RoleTest->with_roles('+quiet', 'Mojo::RoleTest::Hello')->new(name => 'Joel'); + is $obj8->name, 'Joel', 'base attribute'; + is $obj8->whisper, 'psst, joel', 'method from first role'; + is $obj8->hello, 'hello mojo!', 'method from second role'; +}; + +subtest "Classes that are not subclasses of Mojo::Base" => sub { + my $stream = Mojo::ByteStream->with_roles('Mojo::RoleTest::Hello')->new; + is $stream->hello, 'hello mojo!', 'right result'; + + my $c = Mojo::Collection->with_roles('Mojo::RoleTest::Hello')->new; + is $c->hello, 'hello mojo!', 'right result'; + + my $dom = Mojo::DOM->with_roles('Mojo::RoleTest::Hello')->new; + is $dom->hello, 'hello mojo!', 'right result'; + + my $file = Mojo::File->with_roles('Mojo::RoleTest::Hello')->new; + is $file->hello, 'hello mojo!', 'right result'; +}; done_testing(); - From 78f87b5ff20f540516f7c334326b23b4259a20f0 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Sat, 31 Oct 2020 21:09:18 -0700 Subject: [PATCH 5/6] Convert subprocess_ev.t to use subtests --- t/mojo/subprocess_ev.t | 45 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/t/mojo/subprocess_ev.t b/t/mojo/subprocess_ev.t index f7ae4b0664..b2cfaec59a 100644 --- a/t/mojo/subprocess_ev.t +++ b/t/mojo/subprocess_ev.t @@ -10,27 +10,28 @@ plan skip_all => 'EV 4.32+ required for this test!' unless eval use Mojo::IOLoop; use Mojo::Promise; -# Event loop in subprocess (already running event loop) -my ($fail, $result); -Mojo::IOLoop->next_tick(sub { - Mojo::IOLoop->subprocess( - sub { - my $result; - my $promise = Mojo::Promise->new; - $promise->then(sub { $result = shift }); - Mojo::IOLoop->next_tick(sub { $promise->resolve(25) }); - $promise->wait; - return $result; - }, - sub { - my ($subprocess, $err, $twenty_five) = @_; - $fail = $err; - $result = $twenty_five; - } - ); -}); -Mojo::IOLoop->start; -ok !$fail, 'no error'; -is $result, 25, 'right result'; +subtest "Event loop in subprocess (already running event loop)" => sub { + my ($fail, $result); + Mojo::IOLoop->next_tick(sub { + Mojo::IOLoop->subprocess( + sub { + my $result; + my $promise = Mojo::Promise->new; + $promise->then(sub { $result = shift }); + Mojo::IOLoop->next_tick(sub { $promise->resolve(25) }); + $promise->wait; + return $result; + }, + sub { + my ($subprocess, $err, $twenty_five) = @_; + $fail = $err; + $result = $twenty_five; + } + ); + }); + Mojo::IOLoop->start; + ok !$fail, 'no error'; + is $result, 25, 'right result'; +}; done_testing; From 9f70a6d173b29ebbc3664a3f32e5f8f309d58237 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Sat, 31 Oct 2020 21:09:20 -0700 Subject: [PATCH 6/6] Convert tls.t to use subtests --- t/mojo/tls.t | 89 +++++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/t/mojo/tls.t b/t/mojo/tls.t index 64bfe1c4b1..72afff9ac4 100644 --- a/t/mojo/tls.t +++ b/t/mojo/tls.t @@ -11,47 +11,52 @@ plan skip_all => 'IO::Socket::SSL 2.009+ required for this test!' unless Moj use Mojo::IOLoop; use Socket; -# Built-in certificate -socketpair(my $client_sock, my $server_sock, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "Couldn't create socket pair: $!"; -$client_sock->blocking(0); -$server_sock->blocking(0); -my $delay = Mojo::IOLoop->delay; -my $server = Mojo::IOLoop::TLS->new($server_sock); -$server->once(upgrade => $delay->begin); -$server->once(error => sub { warn pop }); -$server->negotiate({server => 1}); -my $client = Mojo::IOLoop::TLS->new($client_sock); -$client->once(upgrade => $delay->begin); -$client->once(error => sub { warn pop }); -$client->negotiate(tls_verify => 0x00); -my ($client_result, $server_result); -$delay->then(sub { ($server_result, $client_result) = @_ }); -$delay->wait; -is ref $client_result, 'IO::Socket::SSL', 'right class'; -is ref $server_result, 'IO::Socket::SSL', 'right class'; - -# Built-in certificate (custom event loop and cipher) -my $loop = Mojo::IOLoop->new; -socketpair(my $client_sock2, my $server_sock2, AF_UNIX, SOCK_STREAM, PF_UNSPEC) - or die "Couldn't create socket pair: $!"; -$client_sock2->blocking(0); -$server_sock2->blocking(0); -$delay = $loop->delay; -$server = Mojo::IOLoop::TLS->new($server_sock2)->reactor($loop->reactor); -$server->once(upgrade => $delay->begin); -$server->once(error => sub { warn pop }); -$server->negotiate(server => 1, tls_ciphers => 'AES256-SHA:ALL'); -$client = Mojo::IOLoop::TLS->new($client_sock2)->reactor($loop->reactor); -$client->once(upgrade => $delay->begin); -$client->once(error => sub { warn pop }); -$client->negotiate(tls_verify => 0x00); -$client_result = $server_result = undef; -$delay->then(sub { ($server_result, $client_result) = @_ }); -$delay->wait; -is ref $client_result, 'IO::Socket::SSL', 'right class'; -is ref $server_result, 'IO::Socket::SSL', 'right class'; -my $expect = $server_result->get_sslversion eq 'TLSv1_3' ? 'TLS_AES_256_GCM_SHA384' : 'AES256-SHA'; -is $client_result->get_cipher, $expect, "$expect has been negotiatied"; -is $server_result->get_cipher, $expect, "$expect has been negotiatied"; +subtest "Built-in certificate" => sub { + socketpair(my $client_sock, my $server_sock, AF_UNIX, SOCK_STREAM, PF_UNSPEC) + or die "Couldn't create socket pair: $!"; + $client_sock->blocking(0); + $server_sock->blocking(0); + my $delay = Mojo::IOLoop->delay; + my $server = Mojo::IOLoop::TLS->new($server_sock); + $server->once(upgrade => $delay->begin); + $server->once(error => sub { warn pop }); + $server->negotiate({server => 1}); + my $client = Mojo::IOLoop::TLS->new($client_sock); + $client->once(upgrade => $delay->begin); + $client->once(error => sub { warn pop }); + $client->negotiate(tls_verify => 0x00); + my ($client_result, $server_result); + $delay->then(sub { ($server_result, $client_result) = @_ }); + $delay->wait; + is ref $client_result, 'IO::Socket::SSL', 'right class'; + is ref $server_result, 'IO::Socket::SSL', 'right class'; +}; + +subtest "Built-in certificate (custom event loop and cipher)" => sub { + my $loop = Mojo::IOLoop->new; + socketpair(my $client_sock2, my $server_sock2, AF_UNIX, SOCK_STREAM, PF_UNSPEC) + or die "Couldn't create socket pair: $!"; + $client_sock2->blocking(0); + $server_sock2->blocking(0); + my $delay = $loop->delay; + my $server = Mojo::IOLoop::TLS->new($server_sock2)->reactor($loop->reactor); + $server->once(upgrade => $delay->begin); + $server->once(error => sub { warn pop }); + $server->negotiate(server => 1, tls_ciphers => 'AES256-SHA:ALL'); + my $client = Mojo::IOLoop::TLS->new($client_sock2)->reactor($loop->reactor); + $client->once(upgrade => $delay->begin); + $client->once(error => sub { warn pop }); + $client->negotiate(tls_verify => 0x00); + my ($client_result, $server_result); + $client_result = $server_result = undef; + $delay->then(sub { ($server_result, $client_result) = @_ }); + $delay->wait; + is ref $client_result, 'IO::Socket::SSL', 'right class'; + is ref $server_result, 'IO::Socket::SSL', 'right class'; + + my $expect = $server_result->get_sslversion eq 'TLSv1_3' ? 'TLS_AES_256_GCM_SHA384' : 'AES256-SHA'; + is $client_result->get_cipher, $expect, "$expect has been negotiatied"; + is $server_result->get_cipher, $expect, "$expect has been negotiatied"; +}; done_testing;